基本汇编引擎

sky123

Keystone

Keystone 是一个开源的汇编器(assembler),用于生成二进制机器码。它支持多种 CPU 架构和操作模式,并且具有高效、轻量级、跨平台等特点,适合用在逆向工程、漏洞研究、二进制分析等领域。

keystone-engine 是 Keystone 项目的 Python 绑定,它允许开发者在 Python 环境中使用 Keystone 汇编器的强大功能。通过 keystone-engine,你可以在 Python 中执行汇编代码,将汇编转换为机器码,或者将机器码反汇编成汇编代码。

Keystone 官方文档

Keystone 安装

keystone-engine 库已经发布到 PyPI(Python Package Index),所以你可以直接使用 pip 来安装它:

1
pip install keystone-engine

注意

这里安装的是 keystone-engine 而不是 keystonekeystone 是另外一个模块。

基本使用

创建 Keystone 对象

要开始使用 Keystone,我们首先需要创建一个 Keystone 对象。这个对象将负责后续的汇编和反汇编工作。

1
2
3
from keystone import Ks, KS_ARCH_X86, KS_MODE_32

ks = Ks(KS_ARCH_X86, KS_MODE_32)

Ks 构造函数的两个参数是 架构类型模式类型。这两个参数定义了要汇编或反汇编的目标架构和模式。

  • 架构类型指定了目标处理器的架构。Keystone 支持多种不同的处理器架构,包括:

    • KS_ARCH_ARM:ARM 处理器架构
    • KS_ARCH_ARM64:ARM 64 位处理器架构
    • KS_ARCH_MIPS:MIPS 处理器架构
    • KS_ARCH_X86:x86 处理器架构
    • KS_ARCH_PPC:PowerPC 处理器架构
    • KS_ARCH_SPARC:SPARC 处理器架构
    • KS_ARCH_SYSTEMZ:SystemZ 架构(IBM Z 系列)
    • KS_ARCH_HEXAGON:Hexagon 架构(Qualcomm Hexagon DSP)
    • KS_ARCH_EVM:EVM 架构(TCS/EVM 虚拟机)
  • 模式类型用于指定特定架构下的工作模式(如 32 位或 64 位模式),Keystone 在同一架构下支持多种模式。

    提示

    在 Keystone 中,如果你希望同时设置多个模式,可以通过 按位“或”操作 (|) 来组合不同的模式。这样可以在一个构造函数中同时指定多个模式类型。

    • 首先是最基本的大小端序,Keystone 默认是小端序

      • KS_MODE_LITTLE_ENDIAN :小端模式(默认)。
      • KS_MODE_BIG_ENDIAN :大端模式。
    • 另外就是一些针对特定架构下的模式类型。

      • ARM 架构相关模式:

        • KS_MODE_ARM :ARM 模式。
        • KS_MODE_THUMB :Thumb 模式(ARM 特有,表示压缩指令集模式)。
        • KS_MODE_V8 :ARMv8 模式(用于支持 ARMv8 架构)。
      • MIPS 架构相关模式:

        • KS_MODE_MIPS3 :MIPS3 模式。

        • KS_MODE_MIPS32R6 :MIPS32R6 模式。

        • KS_MODE_MIPS32 :MIPS32 模式。

        • KS_MODE_MIPS64 :MIPS64 模式。

      • x86 架构相关模式:

        • KS_MODE_16 :16 位模式。

        • KS_MODE_32 :32 位模式。

        • KS_MODE_64 :64 位模式。

      • PowerPC 架构相关模式:

        • KS_MODE_PPC32 :PowerPC 32 位模式。

        • KS_MODE_PPC64 :PowerPC 64 位模式。

        • KS_MODE_QPX :QPX 模式(专门用于某些 PowerPC 处理器)。

      • SPARC 架构相关模式:

        • KS_MODE_SPARC32 :SPARC 32 位模式。

        • KS_MODE_SPARC64 :SPARC 64 位模式。

        • KS_MODE_V9 :SPARC V9 模式(64 位)。

这里给出我们在逆向辅助工具开发中常用的 Keystone 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from keystone import *

# X86-64
from keystone.x86_const import *
ks = Ks(KS_ARCH_X86, KS_MODE_64)

# ARM
from keystone.arm_const import *
ks = Ks(KS_ARCH_ARM, KS_MODE_ARM)
ks = ks(KS_ARCH_ARM, KS_MODE_THUMB)

# AARCH64
from keystone.arm64_const import *
ks = ks(KS_ARCH_ARM64, KS_MODE_ARM)

汇编

Keystone 对象只有一个公开的成员函数 asm,该函数定义如下:

1
def asm(self, string: str, addr: int = 0, as_bytes: bool = False) -> tuple:

参数:

  • string: str :表示输入参数 string 是一个字符串,包含汇编语言的代码。

  • addr: int = 0addr 是一个整数类型的参数,表示机器码开始的内存地址,默认值为 0

  • as_bytes: bool = Falseas_bytes 是一个布尔值,表示是否返回机器码的字节表示。默认值为 False,即返回一个整数列表。

返回值:该函数返回一个元组,元组包含两个元素。

  • encoded :机器码的字节表示。
    • 如果 as_bytes=True,返回 bytes 类型。
    • 如果 as_bytes=False,返回 list[int] 类型,表示机器码的字节序列对应的整数。
  • count:生成的机器码指令的数量。

示例:

1
2
3
4
5
6
7
8
>>> from keystone import *
>>> ks = Ks(KS_ARCH_X86, KS_MODE_64)

>>> ks.asm("inc rax; inc rax")
([72, 255, 192, 72, 255, 192], 2)

>>> ks.asm("inc rax; inc rax", as_bytes=True)
(b'H\xff\xc0H\xff\xc0', 2)

提示

默认情况下,Keystone 使用 Intel 语法解析 x86 架构的汇编指令,如果要切换为 AT&T 语法,可以使用以下代码:

1
2
ks = Ks(KS_ARCH_X86, KS_MODE_32)
ks.syntax = KS_OPT_SYNTAX_ATT

Capstone

Capstone 是一个流行的反汇编框架,它与 Keystone 密切相关,但其功能侧重点不同。Keystone 主要负责将汇编代码转换为机器码(即汇编器),而 Capstone 则是一个反汇编库,专注于将机器码转换为汇编代码(即反汇编器)。

Capstone 官方文档

Capstone 安装

Capstone 提供了 Python 绑定,可以通过 pip 安装:

1
pip install capstone

基本使用

创建 Capstone 对象

首先,你需要创建一个 Capstone 反汇编对象,并设置目标架构和模式。Cs 类用于创建 Capstone 对象,CS_ARCH_*CS_MODE_* 常量用于指定架构和模式。

这里架构和模式定义和 Keystone 库的类似,但具体类别稍有不同。不过对于我们绝大多数逆向需求来说我们不需要关系两者的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from capstone import *

# X86-64
from capstone.x86_const import *
cs = Cs(CS_ARCH_X86, CS_MODE_64)

# ARM
from capstone.arm_const import *
cs = Cs(CS_ARCH_ARM, CS_MODE_ARM)
cs = Cs(CS_ARCH_ARM, CS_MODE_THUMB)

# AARCH64
from capstone.arm64_const import *
cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM)

反汇编

我们可以使用 Capstone 反汇编对象的 disasm 方法来进行反汇编,该函数定义如下:

1
def disasm(self, code, offset, count=0) -> 'CsInsn':

参数:

  • code: bytes :表示输入参数 code 是一个字节串(bytes 类型),包含要反汇编的机器码。这个字节串通常来自二进制文件或内存中的机器码表示。

  • offset: intoffset 是一个整数类型的参数,表示机器码开始的地址(通常是内存中的起始地址)。这个参数是必需的,用来确定反汇编指令的起始位置,帮助计算每条指令的地址。

  • count: int = 0count 是一个整数类型的参数,表示最多反汇编的指令数。默认值为 0,表示反汇编尽可能多的指令,直到代码结束或者遇到无效指令。如果设置了非零的 count,则最多反汇编 count 条指令。

返回值: 该函数返回一个生成器,每次迭代返回一个 CsInsn 对象,代表一条反汇编指令。CsInsn 对象包含以下信息:

  • address :指令的地址,表示这条指令在内存中的位置(十六进制格式)。
  • mnemonic :指令的助记符(如:movaddpush 等)。
  • op_str :指令的操作数(如:eax, ebxmem[rax])。
  • size :指令的字节大小,表示该指令在内存中占用了多少字节。

示例:

1
2
3
4
5
6
7
>>> from capstone import *
>>> cs = Cs(CS_ARCH_X86, CS_MODE_64)
>>> for insn in cs.disasm(bytes([72, 255, 192, 72, 255, 192]),0):
... print(f"{insn.mnemonic} {insn.op_str}")
...
inc rax
inc rax

提示

默认情况下,Capstone 使用 Intel 语法解析 x86 架构的汇编指令,如果要切换为 AT&T 语法,可以使用以下代码:

1
2
cs = Cs(CS_ARCH_X86, CS_MODE_32)
cs.syntax = CS_OPT_SYNTAX_ATT

disasm 的函数定义我们发现,Capstone 不仅仅是简单的将机器码转为汇编指令,而是提供了一个描述汇编指令的对象 CsInsn,从而对汇编指令提供更细粒度的描述。

因此在打印汇编的时候我们需要手动将 CsInsn.mnemonicCsInsn.op_str 拼接成一条汇编。

1
2
for insn in cs.disasm(machine_code, address):
print(f"{insn.address:#x}: {insn.mnemonic} {insn.op_str}")

另外由于 disasm 返回的是一个迭代器,因此我们还可以作如下封装:

1
2
3
4
5
def disasm(machine_code, address=0):
asm_code = ""
for insn in cs.disasm(machine_code, address):
asm_code += "{:8s} {};\n".format(insn.mnemonic, insn.op_str)
return asm_code.strip('\n')

另外除了上述四个成员外,如果我们开启 Cs.detail = True 选项时,CsInsn 还会多一个名为 operands 的字段,这个字段对于不同的架构是不同的。CsInsnoperands 在我们逆向辅助工具编写过程中十分重要,后面会有单独一节介绍。

当然如果我们将机器码反汇编得到 CsInsn 对象,尤其是 Cs.detail 选项开启时性能损耗将相当大。如果我们只需要基本数据,如地址、大小、助记符和操作数,我们可以使用更轻量的 API disasm_lite()

1
def disasm_lite(self, code: bytes, offset: int, count: int = 0) -> Generator[Tuple[int, int, str, str], None, None]:

从版本 2.1 开始,Python 绑定提供了这个新的 disasm_lite() 方法。与 disasm() 不同,disasm_lite() 只返回一个包含 (address, size, mnemonic, op_str) 的元组。基准测试显示,这个轻量级 API 比 disasm() 快最多 30%。

CsInsn 对象

CsInsn 是 Capstone 库中用于表示反汇编指令的对象。在反汇编过程中,CsInsn 对象提供了每条指令的详细信息。通过 CsInsn,你可以访问与该指令相关的各种信息,如指令地址、助记符、操作数、寄存器访问、指令分组等。

提示

为了描述不同架构的指令,CsInsn 的成员比较多样,并且在的架构、指令不同时这成员也不同。因此这里给出一段辅助代码,可以方便在编写代码的时候快速查看 CsInsn 中的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# -*- coding: utf-8 -*-
"""
Capstone 指令详情打印工具(字段名严格对齐 + 类型感知 + 宏名注释)
Python 3.8+,Capstone v4/v5 皆可运行
"""

import capstone as cs
from capstone import Cs, CsInsn

# ---------- 安全获取架构常量 ----------
def _get_arch(name):
return getattr(cs, name, None)

CS_ARCH_X86 = _get_arch("CS_ARCH_X86")
CS_ARCH_ARM = _get_arch("CS_ARCH_ARM")
CS_ARCH_ARM64 = _get_arch("CS_ARCH_ARM64")
CS_ARCH_MIPS = _get_arch("CS_ARCH_MIPS")
CS_ARCH_PPC = _get_arch("CS_ARCH_PPC")
CS_ARCH_RISCV = _get_arch("CS_ARCH_RISCV")
CS_ARCH_SPARC = _get_arch("CS_ARCH_SPARC")
CS_ARCH_SYSZ = _get_arch("CS_ARCH_SYSZ")
CS_ARCH_M68K = _get_arch("CS_ARCH_M68K")
CS_ARCH_M680X = _get_arch("CS_ARCH_M680X")
CS_ARCH_TMS320C64X = _get_arch("CS_ARCH_TMS320C64X")
CS_ARCH_XCORE = _get_arch("CS_ARCH_XCORE")
CS_ARCH_EVM = _get_arch("CS_ARCH_EVM")
CS_ARCH_BPF = _get_arch("CS_ARCH_BPF")
CS_ARCH_WASM = _get_arch("CS_ARCH_WASM")
CS_ARCH_MOS65XX = _get_arch("CS_ARCH_MOS65XX")
CS_ARCH_SH = _get_arch("CS_ARCH_SH")
CS_ARCH_TRICORE = _get_arch("CS_ARCH_TRICORE")
CS_ARCH_LOONGARCH = _get_arch("CS_ARCH_LOONGARCH") # 旧版多半没有

# ---------- 动态导入各架构常量模块(无则 None) ----------
def _try_import(*modnames):
for name in modnames:
try:
return __import__(name, fromlist=['*'])
except Exception:
pass
return None

A64 = _try_import("capstone.arm64_const", "capstone.arm64")
ARM = _try_import("capstone.arm_const", "capstone.arm")
X86 = _try_import("capstone.x86_const", "capstone.x86")
MIPS = _try_import("capstone.mips_const", "capstone.mips")
PPC = _try_import("capstone.ppc_const", "capstone.ppc")
RISCV = _try_import("capstone.riscv_const", "capstone.riscv")
SPARC = _try_import("capstone.sparc_const", "capstone.sparc")

# ---------- 枚举/宏名反查 ----------
def _build_enum_map(module, prefix):
d = {}
if module is None:
return d
try:
for k in dir(module):
if k.startswith(prefix):
try:
d[getattr(module, k)] = k
except Exception:
pass
except Exception:
pass
return d

# 通用
CS_OP_NAMES = _build_enum_map(cs, "CS_OP_") or {0:"CS_OP_INVALID",1:"CS_OP_REG",2:"CS_OP_IMM",3:"CS_OP_MEM",4:"CS_OP_FP"}
CS_AC_NAMES = _build_enum_map(cs, "CS_AC_")
CS_GRP_NAMES= _build_enum_map(cs, "CS_GRP_")

# x86
X86_REG_NAMES = _build_enum_map(X86, "X86_REG_")
X86_INS_NAMES = _build_enum_map(X86, "X86_INS_")
X86_GRP_NAMES = _build_enum_map(X86, "X86_GRP_")
X86_EF_NAMES = _build_enum_map(X86, "X86_EFLAGS_") # 位掩码

# ARM64
A64_REG_NAMES = _build_enum_map(A64, "ARM64_REG_")
A64_INS_NAMES = _build_enum_map(A64, "ARM64_INS_")
A64_GRP_NAMES = _build_enum_map(A64, "ARM64_GRP_")
A64_CC_NAMES = _build_enum_map(A64, "ARM64_CC_")
A64_SFT_NAMES = _build_enum_map(A64, "ARM64_SFT_")
A64_EXT_NAMES = _build_enum_map(A64, "ARM64_EXT_")
A64_VAS_NAMES = _build_enum_map(A64, "ARM64_VAS_")
A64_BAR_NAMES = _build_enum_map(A64, "ARM64_BARRIER_")
A64_HNT_NAMES = _build_enum_map(A64, "ARM64_HINT_")

# ARM
ARM_REG_NAMES = _build_enum_map(ARM, "ARM_REG_")
ARM_INS_NAMES = _build_enum_map(ARM, "ARM_INS_")
ARM_GRP_NAMES = _build_enum_map(ARM, "ARM_GRP_")
ARM_CC_NAMES = _build_enum_map(ARM, "ARM_CC_")
ARM_SFT_NAMES = _build_enum_map(ARM, "ARM_SFT_")

# MIPS
MIPS_REG_NAMES = _build_enum_map(MIPS, "MIPS_REG_")
MIPS_INS_NAMES = _build_enum_map(MIPS, "MIPS_INS_")
MIPS_GRP_NAMES = _build_enum_map(MIPS, "MIPS_GRP_")

# PPC
PPC_REG_NAMES = _build_enum_map(PPC, "PPC_REG_")
PPC_INS_NAMES = _build_enum_map(PPC, "PPC_INS_")
PPC_GRP_NAMES = _build_enum_map(PPC, "PPC_GRP_")

# RISCV
RISCV_REG_NAMES = _build_enum_map(RISCV, "RISCV_REG_")
RISCV_INS_NAMES = _build_enum_map(RISCV, "RISCV_INS_")
RISCV_GRP_NAMES = _build_enum_map(RISCV, "RISCV_GRP_")
RISCV_RM_NAMES = _build_enum_map(RISCV, "RISCV_RM_")

# SPARC
SPARC_REG_NAMES = _build_enum_map(SPARC, "SPARC_REG_")
SPARC_INS_NAMES = _build_enum_map(SPARC, "SPARC_INS_")
SPARC_GRP_NAMES = _build_enum_map(SPARC, "SPARC_GRP_")

# ---------- 工具 ----------
def obj_name(obj):
return getattr(obj, "__capstone_printer_name__", obj.__class__.__name__)

def set_obj_alias(obj, alias):
setattr(obj, "__capstone_printer_name__", alias)

def _hex_or_int(x):
try:
return f"{int(x):#x}"
except Exception:
return str(x)

def _macro_of(value, *maps):
for m in maps:
if m and value in m:
return m[value]
return None

def _reg_macro(insn, reg_id):
arch = getattr(insn._cs, "arch", None)
if arch == CS_ARCH_ARM64: return _macro_of(reg_id, A64_REG_NAMES)
if arch == CS_ARCH_ARM: return _macro_of(reg_id, ARM_REG_NAMES)
if arch == CS_ARCH_X86: return _macro_of(reg_id, X86_REG_NAMES)
if arch == CS_ARCH_MIPS: return _macro_of(reg_id, MIPS_REG_NAMES)
if arch == CS_ARCH_PPC: return _macro_of(reg_id, PPC_REG_NAMES)
if arch == CS_ARCH_RISCV: return _macro_of(reg_id, RISCV_REG_NAMES)
if arch == CS_ARCH_SPARC: return _macro_of(reg_id, SPARC_REG_NAMES)
return None

def _ins_macro(insn, ins_id):
arch = getattr(insn._cs, "arch", None)
if arch == CS_ARCH_ARM64: return _macro_of(ins_id, A64_INS_NAMES)
if arch == CS_ARCH_ARM: return _macro_of(ins_id, ARM_INS_NAMES)
if arch == CS_ARCH_X86: return _macro_of(ins_id, X86_INS_NAMES)
if arch == CS_ARCH_MIPS: return _macro_of(ins_id, MIPS_INS_NAMES)
if arch == CS_ARCH_PPC: return _macro_of(ins_id, PPC_INS_NAMES)
if arch == CS_ARCH_RISCV: return _macro_of(ins_id, RISCV_INS_NAMES)
if arch == CS_ARCH_SPARC: return _macro_of(ins_id, SPARC_INS_NAMES)
return None

def _grp_macro(insn, grp_id):
arch = getattr(insn._cs, "arch", None)
if arch == CS_ARCH_ARM64: return _macro_of(grp_id, A64_GRP_NAMES, CS_GRP_NAMES)
if arch == CS_ARCH_ARM: return _macro_of(grp_id, ARM_GRP_NAMES, CS_GRP_NAMES)
if arch == CS_ARCH_X86: return _macro_of(grp_id, X86_GRP_NAMES, CS_GRP_NAMES)
if arch == CS_ARCH_MIPS: return _macro_of(grp_id, MIPS_GRP_NAMES, CS_GRP_NAMES)
if arch == CS_ARCH_PPC: return _macro_of(grp_id, PPC_GRP_NAMES, CS_GRP_NAMES)
if arch == CS_ARCH_RISCV: return _macro_of(grp_id, RISCV_GRP_NAMES, CS_GRP_NAMES)
if arch == CS_ARCH_SPARC: return _macro_of(grp_id, SPARC_GRP_NAMES, CS_GRP_NAMES)
return _macro_of(grp_id, CS_GRP_NAMES)

def _bitflag_names(value, mapping):
"""将按位掩码展开为 'NAME1|NAME2';mapping: {val: 'NAME'}"""
if not mapping or not isinstance(value, int) or value == 0:
return None
parts = []
# 只选纯位标志(2^k),避免把综合值也算进去
for val, name in mapping.items():
try:
if val and (val & (val - 1)) == 0 and (value & val):
parts.append(name)
except Exception:
pass
if parts:
return "|".join(sorted(parts))
# 若没展开成功,尝试直接匹配整值
return mapping.get(value, None)

# ---------- 架构特有(字段名原样,右侧注释宏) ----------
def handle_x86_operands(insn: CsInsn):
if hasattr(insn, "prefix"):
print(f"insn.prefix = [{', '.join(f'0x{b:02X}' for b in insn.prefix)}]")
if hasattr(insn, "rex"):
print(f"insn.rex = {_hex_or_int(insn.rex)}")
if hasattr(insn, "addr_size"):
print(f"insn.addr_size = {insn.addr_size}")
if hasattr(insn, "modrm"):
print(f"insn.modrm = {_hex_or_int(insn.modrm)}")
if hasattr(insn, "sib"):
print(f"insn.sib = {_hex_or_int(insn.sib)}")
if hasattr(insn, "disp"):
print(f"insn.disp = {_hex_or_int(insn.disp)}")
# eflags
if hasattr(insn, "eflags"):
ef = getattr(insn, "eflags")
names = _bitflag_names(ef, X86_EF_NAMES)
print(f"insn.eflags = {_hex_or_int(ef)}{(' # ' + names) if names else ''}")

def handle_arm64_operands(insn: CsInsn):
if hasattr(insn, "update_flags"):
print(f"insn.update_flags = {bool(insn.update_flags)}")
if hasattr(insn, "cc"):
cc = insn.cc
cm = _macro_of(cc, A64_CC_NAMES)
print(f"insn.cc = {cc} # {cm if cm else ''}")
if hasattr(insn, "barrier"):
b = insn.barrier
bm = _macro_of(b, A64_BAR_NAMES)
print(f"insn.barrier = {b} # {bm if bm else ''}")
if hasattr(insn, "hint"):
h = insn.hint
hm = _macro_of(h, A64_HNT_NAMES)
print(f"insn.hint = {h} # {hm if hm else ''}")
if hasattr(insn, "writeback"):
print(f"insn.writeback = {bool(insn.writeback)}")

def handle_arm_operands(insn: CsInsn):
if hasattr(insn, "update_flags"):
print(f"insn.update_flags = {bool(insn.update_flags)}")
if hasattr(insn, "writeback"):
print(f"insn.writeback = {bool(insn.writeback)}")
if hasattr(insn, "cc"):
cc = insn.cc
cm = _macro_of(cc, ARM_CC_NAMES)
print(f"insn.cc = {cc} # {cm if cm else ''}")

def handle_mips_operands(insn: CsInsn):
if hasattr(insn, "writeback"):
print(f"insn.writeback = {bool(insn.writeback)}")
if hasattr(insn, "branch_delay"):
print(f"insn.branch_delay = {bool(insn.branch_delay)}")

def handle_ppc_operands(insn: CsInsn):
if hasattr(insn, "bc"):
print(f"insn.bc = {insn.bc}")
if hasattr(insn, "update_cr0"):
print(f"insn.update_cr0 = {bool(insn.update_cr0)}")
if hasattr(insn, "update_cr1"):
print(f"insn.update_cr1 = {bool(insn.update_cr1)}")

def handle_riscv_operands(insn: CsInsn):
if hasattr(insn, "writeback"):
print(f"insn.writeback = {bool(insn.writeback)}")
if hasattr(insn, "rounding"):
rm = insn.rounding
rmm = _macro_of(rm, RISCV_RM_NAMES)
print(f"insn.rounding = {rm} # {rmm if rmm else ''}")

def handle_noop(_insn: CsInsn):
pass

def _arch_map(*pairs):
m = {}
for arch_const, handler in pairs:
if isinstance(arch_const, int):
m[arch_const] = handler
return m

ARCH_OPERAND_HANDLERS = _arch_map(
(CS_ARCH_X86, handle_x86_operands),
(CS_ARCH_ARM64, handle_arm64_operands),
(CS_ARCH_ARM, handle_arm_operands),
(CS_ARCH_MIPS, handle_mips_operands),
(CS_ARCH_PPC, handle_ppc_operands),
(CS_ARCH_RISCV, handle_riscv_operands),
(CS_ARCH_SPARC, handle_noop),
(CS_ARCH_SYSZ, handle_noop),
(CS_ARCH_M68K, handle_noop),
(CS_ARCH_M680X, handle_noop),
(CS_ARCH_TMS320C64X, handle_noop),
(CS_ARCH_XCORE, handle_noop),
(CS_ARCH_EVM, handle_noop),
(CS_ARCH_BPF, handle_noop),
(CS_ARCH_WASM, handle_noop),
(CS_ARCH_MOS65XX, handle_noop),
(CS_ARCH_SH, handle_noop),
(CS_ARCH_TRICORE, handle_noop),
(CS_ARCH_LOONGARCH, handle_noop),
)

# ---------- 主打印 ----------
def print_insn_detail(insn: CsInsn):
try:
# 原始反汇编行
bytes_str = " ".join(f"{b:02X}" for b in getattr(insn, "bytes", b""))
print("========== Instruction ==========")
print(f"{insn.address:#x}:\t{bytes_str:<20}\t{insn.mnemonic}\t{insn.op_str}")

# Attributes
print("----- Attributes -----")
print(f"insn.address = {insn.address:#x}")
print(f"insn.bytes = {[f'{b:02X}' for b in getattr(insn, 'bytes', b'')]}")
print(f"insn.mnemonic= {insn.mnemonic}")
print(f"insn.op_str = {insn.op_str}")

# 指令 id 宏
if hasattr(insn, "id"):
im = _ins_macro(insn, insn.id)
print(f"insn.id = {insn.id}{(' # ' + im) if im else ''}")

# 访问寄存器(宏 + 可读名)
if hasattr(insn, "regs_access"):
try:
rds, wrs = insn.regs_access()
if rds:
items = []
for r in rds:
rm = _reg_macro(insn, r)
items.append(f"{r}({(rm + ', ') if rm else ''}{insn.reg_name(r)})")
print("insn.regs_access()[0] =", items)
if wrs:
items = []
for r in wrs:
rm = _reg_macro(insn, r)
items.append(f"{r}({(rm + ', ') if rm else ''}{insn.reg_name(r)})")
print("insn.regs_access()[1] =", items)
except Exception:
pass

# 分组(宏 + 名称)
groups = getattr(insn, "groups", [])
if groups:
items = []
for g in groups:
gm = _grp_macro(insn, g)
try:
gname = insn.group_name(g)
except Exception:
gname = ""
items.append(f"{g}({(gm + ', ') if gm else ''}{gname})")
print("insn.groups =", items)

# 架构特有字段
arch = getattr(insn._cs, "arch", None)
handler = ARCH_OPERAND_HANDLERS.get(arch, handle_noop)
handler(insn)

# Operands(类型感知 + 宏注释)
print("----- Operands -----")
ops = getattr(insn, "operands", [])
for i, op in enumerate(ops):
print(f"[Operand {i}]")
set_obj_alias(op, f"operands[{i}]")

# 操作数类型(带 CS_OP_*)
op_type = getattr(op, "type", None)
opname = CS_OP_NAMES.get(op_type, "CS_OP_UNKNOWN")
print(f"{obj_name(op)}.type = {op_type} # {opname}")

if op_type == 1: # CS_OP_REG
rm = _reg_macro(insn, op.reg)
print(f"{obj_name(op)}.reg = {op.reg} # {(rm + ', ') if rm else ''}{insn.reg_name(op.reg)}")

# 仅对 REG 类型打印 shift/ext/向量信息;且跳过 INVALID/-1
if hasattr(op, "shift") and op.shift and getattr(op.shift, "type", 0):
sft_map = A64_SFT_NAMES if getattr(insn._cs, "arch", None) == CS_ARCH_ARM64 else ARM_SFT_NAMES
stname = _macro_of(op.shift.type, sft_map)
print(f"{obj_name(op)}.shift.type = {op.shift.type} # {stname if stname else ''}")
print(f"{obj_name(op)}.shift.value = {getattr(op.shift, 'value', 0)}")
if hasattr(op, "ext") and getattr(op, "ext", 0):
em = _macro_of(op.ext, A64_EXT_NAMES if getattr(insn._cs, "arch", None) == CS_ARCH_ARM64 else {})
print(f"{obj_name(op)}.ext = {op.ext} # {em if em else ''}")
if hasattr(op, "vas") and getattr(op, "vas", 0):
vm = _macro_of(op.vas, A64_VAS_NAMES if getattr(insn._cs, "arch", None) == CS_ARCH_ARM64 else {})
print(f"{obj_name(op)}.vas = {op.vas} # {vm if vm else ''}")
if hasattr(op, "vector_index") and getattr(op, "vector_index", -1) != -1:
print(f"{obj_name(op)}.vector_index = {op.vector_index}")

elif op_type == 2: # CS_OP_IMM
print(f"{obj_name(op)}.imm = {_hex_or_int(op.imm)}")

elif op_type == 3: # CS_OP_MEM
mem = getattr(op, "mem", None)
if mem:
set_obj_alias(mem, f"{obj_name(op)}.mem")
seg = getattr(mem, "segment", 0)
base = getattr(mem, "base", 0)
idx = getattr(mem, "index", 0)
segm = _reg_macro(insn, seg) if seg else None
basem= _reg_macro(insn, base) if base else None
idxm = _reg_macro(insn, idx) if idx else None
if seg:
print(f"{obj_name(mem)}.segment = {seg} # {(segm + ', ') if segm else ''}{insn.reg_name(seg)}")
if base:
print(f"{obj_name(mem)}.base = {base} # {(basem + ', ') if basem else ''}{insn.reg_name(base)}")
if idx:
print(f"{obj_name(mem)}.index = {idx} # {(idxm + ', ') if idxm else ''}{insn.reg_name(idx)}")
if hasattr(mem, "scale"):
print(f"{obj_name(mem)}.scale = {mem.scale}")
if hasattr(mem, "disp") and getattr(mem, "disp", 0):
print(f"{obj_name(mem)}.disp = {_hex_or_int(mem.disp)}")
# 操作数访问属性(按位展开 CS_AC_*)
if hasattr(op, "access"):
names = _bitflag_names(op.access, CS_AC_NAMES)
print(f"{obj_name(op)}.access = {op.access}{(' # ' + names) if names else ''}")

# 其它可能存在的字段(仅当存在且有意义时打印)
for extra in ("lshift", "post_index", "writeback", "offset"):
if hasattr(mem, extra):
val = getattr(mem, extra)
if val not in (None, 0, False):
print(f"{obj_name(mem)}.{extra} = {val}")

elif op_type == 4: # CS_OP_FP
print(f"{obj_name(op)}.fp = {op.fp}")

else: # 兜底:只打印有值的常见字段
for fld in ("reg", "imm", "fp"):
if hasattr(op, fld):
val = getattr(op, fld)
if fld == "reg":
rm = _reg_macro(insn, val)
print(f"{obj_name(op)}.{fld} = {val} # {(rm + ', ') if rm else ''}{insn.reg_name(val)}")
elif fld == "imm":
print(f"{obj_name(op)}.{fld} = {_hex_or_int(val)}")
else:
print(f"{obj_name(op)}.{fld} = {val}")

print() # operand 分隔

print("=================================\n")
except Exception as e:
print(f"Error processing instruction at 0x{getattr(insn, 'address', 0):x}: {e}\n")

# ----------------- 演示(可删除) -----------------
if __name__ == "__main__":
# x86-64 demo
try:
from capstone import CS_MODE_64
code = b"\x48\x8B\x05\x11\x22\x33\x00" # mov rax, [rip+0x332211]
if CS_ARCH_X86 is not None:
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True
for insn in md.disasm(code, 0x400000):
print_insn_detail(insn)
except Exception as _e:
print("x86-64 demo skipped:", _e)

# AArch64 demo
try:
from capstone import CS_MODE_ARM
code = b"\x21\x7C\x02\x9B" # mul x1, x1, x2
if CS_ARCH_ARM64 is not None:
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
md.detail = True
for insn in md.disasm(code, 0x400000):
print_insn_detail(insn)
except Exception as _e:
print("ARM64 demo skipped:", _e)

基本字段

CsInsn 对象提供了对反汇编指令的基本信息的访问,主要包括以下四个基本字段:

  • address :指令的地址,表示这条指令在内存中的位置(十六进制格式)。
  • mnemonic :指令的助记符(如:movaddpush 等)。
  • op_str :指令的操作数(如:eax, ebxmem[rax])。
  • size :指令的字节大小,表示该指令在内存中占用了多少字节。

寄存器读写

CsInsn 还提供了关于寄存器操作的详细信息。通过该信息可以知道指令涉及到哪些寄存器,并且是如何使用它们的:

  • regs_read() :该方法返回一个指令中被读取的寄存器的集合,通常是一个整数列表,代表寄存器的编号。

  • regs_write() :该方法返回一个指令中被写入的寄存器的集合,通常也是一个整数列表,表示写入的寄存器编号。

  • reg_read(reg_id) :该方法用于检查特定的寄存器是否被指令读取。传入寄存器的 ID,如果该寄存器被读取,返回 True,否则返回 False

  • reg_write(reg_id) :该方法用于检查特定的寄存器是否被指令写入。传入寄存器的 ID,如果该寄存器被写入,返回 True,否则返回 False

  • regs_access() :返回一个元组,包含 regs_read()regs_write() 两个方法的结果。

提示

这里得到的寄存器 ID 不是很直观,CsInsn 有一个 reg_name 方法可以将寄存器编号转换为寄存器的名称。

操作数字段

操作数结构

CsInsn 中的 operands 字段包含了与指令相关的操作数。由于一条指令可能涉及多个操作数,因此 operands 是一个数组其中每一个元素对应一个操作数,顺序和指令中操作数的顺序相同。

由于操作数是 Capstone 底层库获取到的,因此实际上这里 operands 中的元素是定义的 ctypes 结构体。对于不同的架构这个结构体的成员不同。

以 X86 架构为例,operands 中的元素是的类型 X86Op 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class X86OpMem(ctypes.Structure):
# 该类表示x86架构中的内存操作数结构。
# 定义了用于内存寻址的不同部分:段、基址、索引、缩放因子和偏移量
_fields_ = (
('segment', ctypes.c_uint), # 内存段(如:段寄存器,通常为0)
('base', ctypes.c_uint), # 基址寄存器(如:eax、ebx等)
('index', ctypes.c_uint), # 索引寄存器(通常用于数组或结构体寻址)
('scale', ctypes.c_int), # 缩放因子(例如:4,用于字节对齐)
('disp', ctypes.c_int64), # 偏移量(例如:+0x10或-8)
)

class X86OpValue(ctypes.Union):
# 该类定义了x86操作数的值,可以是寄存器、立即数或内存。
# 使用ctypes.Union以节省内存,只能使用其中一个值
_fields_ = (
('reg', ctypes.c_uint), # 寄存器(如:eax、ebx等)
('imm', ctypes.c_int64), # 立即数(如:0x10、42等)
('mem', X86OpMem), # 内存操作数,包含内存段、基址、索引、缩放因子和偏移量
)

class X86Op(ctypes.Structure):
# 该类表示x86指令中的操作数,包含了操作数的类型、值、大小、访问权限等信息
_fields_ = (
('type', ctypes.c_uint), # 操作数类型,表示该操作数是寄存器、立即数、内存等
('value', X86OpValue), # 操作数的具体值(寄存器、立即数或内存)
('size', ctypes.c_uint8), # 操作数的大小(例如:32位或64位)
('access', ctypes.c_uint8), # 操作数的访问类型(读、写或读写)
('avx_bcast', ctypes.c_uint), # AVX广播类型
('avx_zero_opmask', ctypes.c_bool), # AVX零操作掩码
)

@property
def imm(self):
# 返回立即数值
return self.value.imm

@property
def reg(self):
# 返回寄存器编号
return self.value.reg

@property
def mem(self):
# 返回内存操作数
return self.value.mem

从上述代码中可以看出,操作数可以是寄存器、立即数、内存地址等不同的类型。Capstone 通过操作数结构体中的 type 字段来描述操作数的类型,见的操作数类型有(当然除了这些之外还有一些各架构独有的操作数类型,这些类型的名称通常为 <架构>_OP_<类型>,例如 ARM64_OP_CIMM):

  • CS_OP_REG :表示寄存器操作数(如 eaxebx)。
  • CS_OP_IMM :表示立即数操作数(如 0x1042)。
  • CS_OP_MEM :表示内存操作数(如 mem[eax+4])。
  • CS_OP_FP :表示浮点数操作数。

对应不同类型的操作数,value 字段作为联合体有 regimmmem 三种类型。我们可以根据操作数的类型从对应的结构中获取我们想要的值。

由于操作数对象提供了 immregmem 三种方法来封装 value 字段的访问,因此我们可以使用例如 insn.operands[1].mem.base 这种方式跳过 value 直接访问联合体中的成员。

内存操作数

因为会经常用到,这里要着重说明一下内存操作数的结构。内存操作数是指令中用于描述内存地址的操作数类型,通常在汇编指令中通过指定内存地址、基址寄存器、索引寄存器和偏移量来进行内存访问。

在 Capstone 库中,内存操作数的类型为 CS_OP_MEM,并且通过 CsOperand 对象的 mem 字段来描述相关的内存信息。还是以前面的 X86OpMem 结构体举例:

1
2
3
4
5
6
7
8
class X86OpMem(ctypes.Structure):
_fields_ = (
('segment', ctypes.c_uint), # 内存段寄存器,通常为 0
('base', ctypes.c_uint), # 基址寄存器,例如 eax、rbx 等
('index', ctypes.c_uint), # 索引寄存器,例如 esi、edi 等
('scale', ctypes.c_int), # 缩放因子,例如 4(字节对齐)
('disp', ctypes.c_int64), # 偏移量,表示地址的固定偏移
)
  • segment :内存段寄存器,在现代架构中通常为 0,表示直接访问内存,而不通过特定的段寄存器。如果涉及到段寻址(例如在 x86 16 位模式下),此字段可能会有所变化。
  • base :基址寄存器,是计算内存地址的基础寄存器。通过将基址寄存器的值与偏移量 disp 相加,可以确定内存地址。例如,eaxebx 通常用于存储基地址。
  • index :索引寄存器,是一个可选的寄存器,通常用于数组、结构体等类型的数据访问。通过 base + (index * scale) + disp 可以计算出最终的内存地址。index 通常用于访问内存中的多个元素,例如数组的每个元素,scale 会控制每次索引跳跃的大小。
  • scale :缩放因子,用于乘以索引寄存器的值,以调整访问的大小。对于数据结构的访问,这个字段决定了如何调整内存访问的步长。例如,访问一个 int 类型数组时,scale 通常为 4(因为一个 int 占 4 字节)。
  • disp :偏移量,表示相对于基址和索引寄存器的地址偏移量。它通常是一个常数,表示在基地址或索引基础上额外的偏移量。例如,[eax + 4] 中的偏移量就是 4
x86 架构

在 x86-64 架构中,内存操作数常常包含基址寄存器、索引寄存器、缩放因子和偏移量,下面的汇编指令是 x86-64 中的一个常见例子:

1
mov rax, [rbx + rcx*4 + 0x10]

这条指令的操作数对应 Capstone 的内存操作数结构如下:

1
2
3
4
5
6
7
8
# 对应 Capstone 解析的结构体
mem = insn.operands[1].mem

print(f"Segment: {mem.segment}") # 通常为 0
print(f"Base: {insn.reg_name(mem.base)}") # rbx
print(f"Index: {insn.reg_name(mem.index)}") # rcx
print(f"Scale: {mem.scale}") # 4
print(f"Disp: {hex(mem.disp)}") # 0x10
  • segment0(通常为 0,因为没有特别指定段寄存器)
  • baserbx(基址寄存器)
  • indexrcx(索引寄存器)
  • scale4(数组或数据元素大小,通常是 4
  • disp0x10(偏移量)

通常来说,X86 从全局变量读取数据的指令为:

1
mov reg, qword ptr ds:[rip + disp]

因此我们可以计算全局变量的地址:

1
insn.operands[1].mem.disp + insn.address + insn.size
ARM 架构

ARM 架构使用类似于 x86 的寻址模式,但是语法和寄存器命名方式不同。例如下面这条汇编指令是将 r1 + (r2 << 2) 计算出的内存地址中的值加载到 r0 寄存器:

1
ldr r0, [r1, r2, lsl #2]

这条指令的操作数对应 Capstone 的内存操作数结构如下:

1
2
3
4
5
6
7
# 对应 Capstone 解析的结构体
mem = op.mem
print(f"Segment: {mem.segment}") # 通常为 0
print(f"Base: {insn.reg_name(mem.base)}") # r1
print(f"Index: {insn.reg_name(mem.index)}") # r2
print(f"Scale: {mem.scale}") # 4 (因为 lsl #2 是左移两位,相当于乘 4)
print(f"Disp: {hex(mem.disp)}") # 0
  • segment0(默认段选择符)
  • baser1(基址寄存器)
  • indexr2(索引寄存器)
  • scale4(因为 lsl #2 是左移 2 位,相当于乘 4)
  • disp0(没有偏移量)

由于 ARM 架构的指令不能直接操作内存,因此无法直接根据全局变量的地址读取全局变量的数据,而是:

  1. 先将编译在函数后面的地址表中的全局变量的地址加载到寄存器中。
  2. 然后还可能跟 pc 寄存器相加顺便实现地址无关代码
  3. 最后再通过寄存器访问全局变量所在的地址读取数据。

也就是通常来说 ARM 架构从全局变量读取数据的指令为:

1
2
3
ldr r1, [pc, #disp]	; 从 (pc + disp) 计算出的内存地址加载数据到 r1
add r1, r1, pc ; 将 pc 加上偏移量后的地址值加到 r1,得到目标地址
ldr r2, [r1] ; 从 r1 指向的内存地址加载数据到 r2

由于编译器的针对 CPU 流水线的优化,这些指令之间可能还会穿插其它的指令。因此我们不能直接通过指令得出全局变量的地址。不过这里我们可以先获取指令序列 insn_list,然后简单模拟一下这个从内存加载数据的过程来计算全局变量的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
reg_value = {}
for insn in insn_list:
# ldr r1, [pc, #0xf0]
if insn.mnemonic == 'ldr' \
and insn.operands[0].type == ARM_OP_REG \
and insn.operands[1].type == ARM_OP_MEM \
and insn.operands[1].mem.base == ARM_REG_PC:
reg_value[insn.operands[0].reg] = idc.get_wide_dword((insn.address + 4 + insn.operands[1].mem.disp) & ~3)
# add r1, pc
elif insn.mnemonic == 'add' \
and insn.operands[0].type == ARM_OP_REG \
and insn.operands[1].type == ARM_OP_REG \
and insn.operands[1].reg == ARM_REG_PC \
and insn.operands[0].reg in reg_value.keys():
reg = insn.operands[0].reg
reg_value[reg] = reg_value[reg] + insn.address + 4
if idc.get_segm_name(reg_value[reg]) == '.rodata': # 全局变量应该放在 .rodata 段中
print(f"global var addr: {reg_value[reg]:#x}")
elif insn.mnemonic.startswith('b'):
reg_value.clear()
AArch64 架构

AArch64 是 ARM 的 64 位版本,它使用与 ARM 相似的寻址模式。AArch64 允许更复杂的寻址模式,包括有符号立即数、寄存器和可选的缩放因子。例如下面这条汇编指令是将 x1 + (x2 << 3) 计算出的内存地址中的值加载到 x0 寄存器:

1
ldr x0, [x1, x2, lsl #3]

这条指令的操作数对应 Capstone 的内存操作数结构如下:

1
2
3
4
5
6
7
# 对应 Capstone 解析的结构体
mem = op.mem
print(f"Segment: {mem.segment}") # 通常为 0
print(f"Base: {insn.reg_name(mem.base)}") # x1
print(f"Index: {insn.reg_name(mem.index)}") # x2
print(f"Scale: {mem.scale}") # 8 (因为 lsl #3 是左移三位,相当于乘 8)
print(f"Disp: {hex(mem.disp)}") # 0
  • segment0(默认段选择符)
  • basex1(基址寄存器)
  • indexx2(索引寄存器)
  • scale8(因为 lsl #3 是左移 3 位,相当于乘 8)
  • disp0(没有偏移量)

AArch64 架构的指令同样不能直接操作内存,因此无法直接根据全局变量的地址读取全局变量的数据,而是:

  1. 通过 adrp 指令将全局变量地址所在内存页的基地址加载到寄存器中。由于 AArch64 是基于页面地址访问的,adrp 将会设置寄存器为地址的页对齐值。
  2. 将存有页基址的寄存器加上某个偏移量,使其指向全局变量。
  3. 通过 ldrldp 等指令从该寄存器指向的内存中读入数据。

也就是通常来说 AArch64 架构从全局变量读取数据的指令为:

1
2
3
adrp x2, #0x37ff000	; 将页面的基地址加载到寄存器 x2 中
add x2, x2, #0x148 ; 将基地址加上偏移量 #0x148 从而计算出完整的内存地址
ldp x4, x5, [x2] ; 从 x2 指向的地址加载两个数据项,分别存入寄存器 x4 和 x5

同样我们可以参考 ARM 架构的做法来计算全局变量的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
reg_value = {}
for insn in insn_list:
# adrp x2, #0x37ff000
if insn.mnemonic == 'adrp' \
and insn.operands[0].type == ARM64_OP_REG \
and insn.operands[1].type == ARM64_OP_IMM:
reg_value[insn.operands[0].reg] = insn.operands[1].imm
# add x2, x2, #0x148
elif insn.mnemonic == 'add' \
and insn.operands[0].type == ARM64_OP_REG \
and insn.operands[1].type == ARM64_OP_REG \
and insn.operands[2].type == ARM64_OP_IMM \
and insn.operands[0].reg == insn.operands[1].reg \
and insn.operands[0].reg in reg_value.keys():
reg = insn.operands[0].reg
reg_value[reg] += insn.operands[2].imm
if idc.get_segm_name(reg_value[reg]) == '.rodata':
print(f"global var addr: {reg_value[reg]:#x}")
elif insn.mnemonic.startswith('b'):
reg_value.clear()
  • Title: 基本汇编引擎
  • Author: sky123
  • Created at : 2025-08-18 23:59:54
  • Updated at : 2025-08-25 01:10:09
  • Link: https://skyi23.github.io/2025/08/18/基本汇编引擎/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments