3. 定位并解析LC_SEGMENT_64
任务目标
在 Mach-O 64 位文件中,LC_SEGMENT_64 加载命令描述了可执行文件如何映射到进程地址空间。本案例将编写一个 Python 脚本,解析 Mach-O 文件头,定位所有 LC_SEGMENT_64 命令,并提取每个段的段名(segname)、虚拟地址(vmaddr)、文件偏移(fileoff)和段权限(maxprot/initprot)。
核心概念
Mach-O 文件由文件头(Header)、加载命令(Load Commands)和数据区组成。64 位 Mach-O 文件头以 magic 值 0xfeedfacf(MH_MAGIC_64)标识。加载命令紧跟在文件头之后,每个命令由 cmd(命令类型)和 cmdsize(命令大小)开头。LC_SEGMENT_64 的值为 0x19。解析时需注意:文件头大小为 32 字节,加载命令需按 8 字节对齐。
完整实现
import struct
MH_MAGIC_64 = 0xfeedfacf
LC_SEGMENT_64 = 0x19
# 64位文件头: magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags, reserved
HEADER_FORMAT = '<IIIIIIII'
HEADER_SIZE = 32
# segment_command_64: cmd, cmdsize, segname[16], vmaddr, vmsize, fileoff, filesize, maxprot, initprot, nsects, flags
SEGMENT_FORMAT = '<II16sQQQQiiII'
SEGMENT_SIZE = 72
def parse_segments(path):
with open(path, 'rb') as f:
header_data = f.read(HEADER_SIZE)
if len(header_data) < HEADER_SIZE:
raise ValueError('文件过小,不是有效的 Mach-O 文件')
magic, _, _, _, ncmds, _, _, _ = struct.unpack(HEADER_FORMAT, header_data)
if magic != MH_MAGIC_64:
raise ValueError(f'不是 64 位 Mach-O 文件 (magic=0x{magic:x})')
segments = []
offset = HEADER_SIZE
for _ in range(ncmds):
f.seek(offset)
cmd_data = f.read(8)
if len(cmd_data) < 8:
break
cmd, cmdsize = struct.unpack('<II', cmd_data)
if cmd == LC_SEGMENT_64:
f.seek(offset)
seg_data = f.read(SEGMENT_SIZE)
if len(seg_data) < SEGMENT_SIZE:
break
_, _, segname_raw, vmaddr, _, fileoff, _, maxprot, initprot, _, _ = \
struct.unpack(SEGMENT_FORMAT, seg_data)
segname = segname_raw.split(b'\x00')[0].decode('utf-8', errors='replace')
segments.append({
'segname': segname,
'vmaddr': hex(vmaddr),
'fileoff': hex(fileoff),
'maxprot': f'0x{maxprot:x}',
'initprot': f'0x{initprot:x}'
})
offset += cmdsize
return segments
if __name__ == '__main__':
import sys
if len(sys.argv) != 2:
print('用法: python parse_segment.py <mach-o文件路径>')
sys.exit(1)
try:
segs = parse_segments(sys.argv[1])
if not segs:
print('未找到 LC_SEGMENT_64 命令')
else:
print(f'共找到 {len(segs)} 个 LC_SEGMENT_64 段:')
for s in segs:
print(f" 段名: {s['segname']:<16} vmaddr: {s['vmaddr']:<12} "
f"fileoff: {s['fileoff']:<12} maxprot: {s['maxprot']} initprot: {s['initprot']}")
except Exception as e:
print(f'解析失败: {e}')
sys.exit(1)
运行与验证
环境要求:Python 3.6+,无需第三方库。
运行方式:
python parse_segment.py /path/to/your/binary
可使用 macOS 系统自带的任意可执行文件(如 /bin/ls)作为测试输入。
验证方法:
- 程序输出应包含
__PAGEZERO、__TEXT、__DATA等典型段名; - 用
otool -l /bin/ls对比输出,段名、vmaddr、fileoff 应与脚本结果一致; - 权限值中
maxprot与initprot通常相同,常见值为0x5(r-x)、0x3(rw-)等。
预期输出示例(以 /bin/ls 为例,实际地址因系统版本而异):
共找到 4 个 LC_SEGMENT_64 段:
段名: __PAGEZERO vmaddr: 0x0 fileoff: 0x0 maxprot: 0x0 initprot: 0x0
段名: __TEXT vmaddr: 0x100000000 fileoff: 0x0 maxprot: 0x5 initprot: 0x5
段名: __DATA vmaddr: 0x100003000 fileoff: 0x3000 maxprot: 0x3 initprot: 0x3
段名: __LINKEDIT vmaddr: 0x100004000 fileoff: 0x4000 maxprot: 0x1 initprot: 0x1
常见问题
- magic 不匹配:确认文件是 64 位 Mach-O(fat 通用二进制需先提取单架构,可用
lipo -thin arm64处理); - 段名乱码:段名字段是固定 16 字节的 C 字符串,代码已按
\x00截断处理; - 对齐问题:加载命令之间可能存在填充字节,脚本通过
cmdsize跳转而非固定步长,可正确处理。