【lua】二进制数据打包和解析

▒ 目录 ▒

    • [🛫 导读](#🛫 导读)
    • [1️⃣ 基础概念:什么是二进制打包/解析?](#1️⃣ 基础概念:什么是二进制打包/解析?)
      • [1.1 控制符(字节序与对齐方式)](#1.1 控制符(字节序与对齐方式))
      • [1.2 类型符(数据类型定义)](#1.2 类型符(数据类型定义))
    • [2️⃣ 基础使用:string.pack 与 string.unpack 示例](#2️⃣ 基础使用:string.pack 与 string.unpack 示例)
      • [2.1 string.pack:打包Lua值为二进制字符串](#2.1 string.pack:打包Lua值为二进制字符串)
      • [2.2 string.unpack:从二进制字符串解析Lua值](#2.2 string.unpack:从二进制字符串解析Lua值)
    • [3️⃣ CE中的实际应用:内存数据读写与解析](#3️⃣ CE中的实际应用:内存数据读写与解析)
      • [3.1 构造二进制数据并写入游戏内存](#3.1 构造二进制数据并写入游戏内存)
      • [3.2 解析游戏内存中的角色属性结构体](#3.2 解析游戏内存中的角色属性结构体)
      • [3.3 CE应用注意事项](#3.3 CE应用注意事项)
    • [🛬 文章小结](#🛬 文章小结)
    • [📖 参考资料](#📖 参考资料)

🛫 导读

需求

在Lua开发(尤其是Cheat Engine/游戏内存操作场景)中,经常需要处理二进制数据 (如内存中的结构体、网络数据包、文件二进制内容等)。string.packstring.unpack 是Lua 5.3+引入的核心函数,分别用于将多个Lua值"打包"为紧凑的二进制字符串,以及从二进制字符串中"解析"出原始Lua值。

本文需满足两类需求:1. 掌握 pack/unpack 的基础语法、格式符规则及对齐逻辑;2. 结合Cheat Engine(CE)实际场景,实现内存数据解析(如读取游戏角色属性)、二进制数据构造(如写入自定义内存数据),解决CE中"结构化操作二进制数据"的核心问题。

版本号 描述
文章日期 2025-08-30
IDE https://www.mycompiler.io/new/lua 5.3

1️⃣ 基础概念:什么是二进制打包/解析?

二进制数据是计算机底层存储、传输的核心格式(如内存、文件、网络流),其特点是"紧凑"(无文本格式的冗余)、"类型关联"(每个字节对应特定数据类型)。

Lua中默认的字符串是"文本安全"的(可存储任意字节,包括\0),string.packstring.unpack 正是连接"Lua高层值"(如数字、字符串)与"底层二进制数据"的桥梁:

  • 打包(pack):将多个分散的Lua值(如int、float、字符串)按指定格式合并为1个二进制字符串,便于写入内存/文件。
  • 解析(unpack):从二进制字符串中,按预设格式提取出原始Lua值,便于读取内存/文件中的结构化数据。

1.1 控制符(字节序与对齐方式)

控制符用于定义整体字节序和对齐规则,需放在格式符字符串开头,影响后续所有类型符:

控制符 含义说明 CE场景适用性
< 强制小端字节序(Little-endian) 首选,Windows游戏内存默认小端
> 强制大端字节序(Big-endian) 少见,多用于网络协议或特定硬件数据
= 使用当前平台原生字节序(如x86/x64为小端) 兼容平台相关数据,需确认目标平台
![n] 设置最大对齐字节数为n(n为整数,如!4表示4字节对齐),默认使用原生对齐 解析结构体时需匹配内存对齐(如游戏结构体按4字节对齐)
(空格) 忽略,仅用于格式符可读性分割(如"< i[4] f""<i[4]f"等价) 推荐使用,提高格式符可读性

1.2 类型符(数据类型定义)

类型符用于定义具体数据类型,需与控制符配合使用。以下为CE场景高频使用的类型符(按使用频率排序):

类型符 数据类型描述 字节数 符号性 CE场景典型用途
b 带符号字节(char) 1 有符号 存储状态标记(如0=禁用,1=启用)
B 无符号字节(char) 1 无符号 存储小范围计数(如道具数量0-255)
h 带符号短整数(short,平台原生大小,通常2字节) 2(x86/x64平台) 有符号 存储中等范围数值(如技能等级-32768~32767)
H 无符号短整数(unsigned short,平台原生大小,通常2字节) 2(x86/x64平台) 无符号 存储非负中等数值(如物品ID 0~65535)
i[n] 带符号整数,指定n字节(n=1/2/4/8,如i[4]表示4字节带符号整数) n(1/2/4/8) 有符号 存储指定长度的有符号数值(如血量-21亿~21亿)
I[n] 无符号整数,指定n字节(n=1/2/4/8,如I[8]表示8字节无符号整数) n(1/2/4/8) 无符号 存储64位游戏内存地址(I[8])或大数值
f 单精度浮点数(float,平台原生大小,通常4字节) 4(x86/x64平台) - 存储精度要求不高的小数(如移动速度1.5)
d 双精度浮点数(double,平台原生大小,通常8字节) 8(x86/x64平台) - 存储高精度小数(如坐标X:12345.6789)
c[n] 固定长度字符串(n字节,不足补\0,超出截断) n - 存储固定长度文本(如角色名,c[10]表示10字节)
z 零终止字符串(C风格,以\0结尾,长度可变) 字符串长度+1(含\0) - 存储游戏内动态文本(如对话、提示信息)
x 填充字节(无实际数据,仅占位对齐) 1 - 结构体中对齐字节(如x3表示3个填充字节)
Xop 空项,仅用于对齐(op为对齐规则,如X4表示按4字节对齐) 0(仅影响对齐,不占实际字节) - 复杂结构体强制对齐(如X4i[4]确保int在4字节对齐位置)

2️⃣ 基础使用:string.pack 与 string.unpack 示例

先通过纯Lua示例掌握核心用法,再延伸到CE场景。所有示例基于Lua 5.3+(CE 7.0+默认集成该版本)。

2.1 string.pack:打包Lua值为二进制字符串

语法:local bin_str = string.pack(format, v1, v2, ..., vn)

  • format:格式符字符串;
  • v1~vn:需打包的Lua值(数量、类型需与格式符匹配);
  • 返回值:二进制字符串(可直接写入内存/文件)。

示例1:打包"角色基础属性"(32位血量、32位蓝量、单精度攻击力、10字节角色名)

lua 复制代码
-- 格式符:<(小端紧凑)+ i4(血量)+ i4(蓝量)+ f(攻击力)+ s10(角色名)
local format = "<i4i4fc10"
local hp = 12500  -- 带符号32位int
local mp = 8700   -- 带符号32位int
local atk = 156.5 -- 32位float
local name = "Warrior" -- 字符串(不足10字节会自动用\0填充)

-- 执行打包
local bin_data = string.pack(format, hp, mp, atk, name)

-- 查看结果:二进制字符串长度 = 4+4+4+10 = 22字节(因用了<强制1字节对齐,无额外填充)
print(#bin_data) -- 输出:22

2.2 string.unpack:从二进制字符串解析Lua值

语法:local v1, v2, ..., vn, next_pos = string.unpack(format, bin_str, [start_pos])

  • format:与打包时一致的格式符;
  • bin_str:待解析的二进制字符串;
  • start_pos:可选,起始解析位置(默认1,Lua字符串索引从1开始);
  • 返回值:解析出的Lua值(v1~vn) + 下一个未解析的位置(next_pos,便于解析长数据)。

示例2:解析示例1中打包的"角色基础属性"

lua 复制代码
-- 格式符:<(小端紧凑)+ i4(血量)+ i4(蓝量)+ f(攻击力)+ s10(角色名)
local format = "<i4i4fc10"
local hp = 12500  -- 带符号32位int
local mp = 8700   -- 带符号32位int
local atk = 156.5 -- 32位float
local name = "Warrior" -- 字符串(不足10字节会自动用\0填充)

-- 执行打包
local bin_data = string.pack(format, hp, mp, atk, name)

-- 查看结果:二进制字符串长度 = 4+4+4+10 = 22字节(因用了<强制1字节对齐,无额外填充)
print(#bin_data) -- 输出:22

-- 执行解析
local hp, mp, atk, name, next_pos = string.unpack(format, bin_data)

-- 查看解析结果
print("血量:", hp)      -- 输出:血量:12500
print("蓝量:", mp)      -- 输出:蓝量:8700
print("攻击力:", atk)   -- 输出:攻击力:156.5
print("角色名:", name)  -- 输出:角色名:Warrior(不足10字节的部分被\0填充,但Lua字符串会保留\0,需按需处理)
print("下一个位置:", next_pos) -- 输出:23(因总长度22,下一个位置是23,无更多数据)

示例3:解析长数据(如多个角色属性)

lua 复制代码
-- 假设bin_data是2个角色的属性(每个22字节,总44字节)
local bin_data_long = bin_data .. bin_data -- 拼接2个角色数据

-- 解析第一个角色
local hp1, mp1, atk1, name1, pos = string.unpack(format, bin_data_long)
-- 从pos位置解析第二个角色
local hp2, mp2, atk2, name2 = string.unpack(format, bin_data_long, pos)

print("角色2血量:", hp2) -- 输出:角色2血量:12500(与第一个角色一致)

3️⃣ CE中的实际应用:内存数据读写与解析

CE的核心能力是"读写目标进程内存",而游戏内存中大量数据以"二进制结构体"形式存储(如角色属性、道具信息)。pack/unpack 可解决CE中"结构化操作内存数据"的痛点------无需手动计算字节偏移,直接按格式解析/构造数据。
假设某32位游戏中,角色属性结构体在内存中的布局如下(通过CE结构体查看器获取):

成员变量 类型 字节数 偏移量(从结构体起始地址开始)
hp 无符号32位int 4 0
mp 无符号32位int 4 4
level 无符号16位int 2 8
speed 32位float 4 10
name 零终止字符串 可变 14

3.1 构造二进制数据并写入游戏内存

需求:修改上述角色的"移动速度"为2.5,"血量"为20000,构造二进制数据后写入内存。
ps: 测试过程中,小编是自己手动创建了一块内存!!!
实现代码(CE Lua脚本):

lua 复制代码
-- 1. 复用进程和地址(同场景1)
local pid = openProcess("CalculatorApp.exe")
if not pid then
 print("未找到游戏进程!")
 return
end

-- 2. 定义新的属性值
local new_hp = 20000    -- 新血量
local new_mp = 15000    -- 保持原蓝量(或从内存读取后修改)
local new_level = 50    -- 保持原等级
local new_speed = 2.5   -- 新移动速度
local new_name = "Hero" -- 新角色名(零终止,pack会自动加\0)

-- 3. 打包新数据(格式符同场景1)
local struct_format = "<I4I4I2fz"
local bin_data = string.pack(struct_format, new_hp, new_mp, new_level, new_speed, new_name)

-- 4. 计算写入长度(仅写入前14字节+name长度+1(\0),避免覆盖后续内存)
-- 解析打包后的name长度,计算总写入长度
local name_len = #new_name
local write_len = 4 + 4 + 2 + 4 + (name_len + 1) -- I4+I4+I2+f + (name+1个\0)
print(write_len, #bin_data)

-- 5. 写入内存(CE函数)
function writeStringArr(struct_addr, s)
  for i = 1, #s do
    print(i, struct_addr+i, s:byte(i))
    writeByte(struct_addr+i-1, s:byte(i))
  end
end
local struct_addr = 0x1F7F5850000
writeStringArr(struct_addr, bin_data)

运行结果如下:

3.2 解析游戏内存中的角色属性结构体

需求:读取内存地址 0x00A1B2C3 处的角色结构体数据,并解析为Lua值。
实现代码(CE Lua脚本):

lua 复制代码
-- 1. 复用进程和地址(同场景1)
local pid = openProcess("CalculatorApp.exe")
if not pid then
 print("未找到游戏进程!")
 return
end

function readStringX(struct_addr, len)
  local ret = ''
  for i = 1, len do
    ret = ret .. string.char(readByte(struct_addr+i-1))
  end

  return ret
end
local struct_addr = 0x1F7F5850000
local s = readStringX(struct_addr, 29+1)
print(#s)

-- 解析二进制数据
local struct_format = "<I4I4I2fz"
local hp, mp, level, speed, name = string.unpack(struct_format, s)
print(hp, mp, level, speed, name)

-- 输出解析结果
print("=== 角色属性 ===")
print("内存地址:0x" .. string.format("%X", struct_addr))
print("血量:", hp)
print("蓝量:", mp)
print("等级:", level)
print("移动速度:", speed)
print("角色名:", name)

3.3 CE应用注意事项

  1. 内存地址合法性 :需确保 struct_addr 是有效内存地址(可通过CE的"指针扫描"获取动态地址,避免静态地址失效);
  2. 格式符与内存类型严格匹配 :若游戏内存中是"无符号32位int",则必须用 I4 而非 i4,否则会解析出负数;
  3. 字节序对齐 :游戏内存(Windows平台)默认是"小端字节序",格式符开头加 < 强制紧凑对齐,避免因自然对齐导致的偏移错误;
  4. 零终止字符串处理z 格式符会自动处理 \0,但需注意读取内存时长度要足够(避免截断 \0,导致解析出乱码);
  5. 64位游戏适配 :64位游戏中内存地址是64位,需用 I8 格式符存储地址,且CE需运行在64位模式下。

🛬 文章小结

  1. string.pack/string.unpack 的核心是格式符 ,需牢记高频类型(i4/I4/f/z/sN)及对齐修饰符(< 首选);
  2. 基础用法遵循"格式符定义结构 → pack打包 → unpack解析"的流程,需确保"打包值数量/类型"与"格式符"完全匹配;
  3. CE场景中,两者的核心价值是"结构化读写内存":通过 readStringX 读二进制 → unpack 解析属性;通过 pack 构造新数据 → writeStringArr 写入,替代手动计算字节偏移的繁琐操作;
  4. 关键避坑点:字节序(小端优先)、内存地址有效性、字符串 \0 处理、64位/32位格式符区分。

📖 参考资料

相关推荐
列星随旋13 小时前
基于 Redis + Lua,实现“多维度原子限流”(令牌桶 + 滑动窗口)
java·redis·lua
上海合宙LuatOS13 小时前
LuatOS扩展库API——【exgnss】GNSS定位
物联网·lua·luatos
0xDevNull13 小时前
Redis Lua 脚本详细教程
redis·缓存·lua
上海合宙LuatOS14 小时前
LuatOS扩展库API——【exlcd】显示屏控制
物联网·lua·luatos
0xDevNull14 小时前
Spring Boot 中使用 Redis Lua 脚本详细教程
spring boot·redis·lua
DJ斯特拉2 天前
Redis使用lua脚本
junit·单元测试·lua
Aktx20FNz2 天前
OpenClaw中级到高级教程
lua
LcGero3 天前
Lua + Cocos Creator 实战:用 Lua 驱动 UI 与游戏逻辑
游戏·ui·lua
静心观复5 天前
Lua 脚本是什么
开发语言·lua
LcGero5 天前
Lua 协程(Coroutine):游戏里的“伪多线程”利器
游戏·lua·游戏开发·协程