【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位格式符区分。

📖 参考资料

相关推荐
夜猫逐梦1 天前
【lua】table基础操作
lua
大飞pkz3 天前
【Lua】题目小练12
开发语言·lua·题目小练
大得3693 天前
国产nginx,tengine,内部已有lua,未安装mysql,安装mysql
运维·nginx·lua
大得3694 天前
nginx结合lua做转发,负载均衡
nginx·junit·lua
SimpleUmbrella5 天前
windows下配置lua环境
c++·lua
朱砂绛6 天前
【大模型本地运行与部署框架】Ollama的API交互
开发语言·lua·交互
郭京京7 天前
go语言redis中使用lua脚本
redis·go·lua
Teamol20208 天前
Lua 语法核心特点
单元测试·lua