Python-Assembler 汇编风格解释器:用 Python 3 编写并运行 Intel 语法汇编程序

Python-Assembler 汇编风格解释器:用 Python 3 编写并运行 Intel 语法汇编程序 Python-Assembler 汇编风格解释器用 Python 3 编写并运行 Intel 语法汇编程序【免费下载链接】PythonMy Python Examples项目地址: https://gitcode.com/gh_mirrors/py/Python本指南围绕当前仓库中的 Assembler 子项目展开讲解一个使用 Python 3 实现的、语法贴近 Intel 汇编的简易解释型语言——Python-Assembler。你将学会它的安装运行方式、寄存器与指令系统、系统中断调用、变量与注释、栈操作、跳转与子程序等全部语言特性并结合 assembler.py 源码理解其解释执行原理。项目概览与运行方式Python-Assembler 不是一个把汇编翻译成机器码的真正汇编器而是一个汇编风格的解释器assembler-like interpreter它直接读取以 Intel 汇编语法书写的源文件逐条解释执行输出结果到控制台。整个解释器仅由一个 Python 文件 assembler.py约 1300 行实现不依赖任何第三方库使用 Python 3 即可运行。启动命令在 Assembler 目录下执行python assembler.py code.txt按下回车后解释器会读取并解释code.txt中的源码。关键特性是支持多个输入文件——你可以一次传入多个文本文件它们会被逐个解释执行python assembler.py code1.txt code2.txt code3.txt从 主函数 的源码可以看到程序通过sys.argv[1:]遍历所有命令行参数第一个参数是程序自身故从[1:]开始每处理一个文件前调用resetInterpreter()重置解释器状态再依次执行loadFile→scan→registerLabels→parser四步流程for arg in sys.argv[1:]: resetInterpreter() # resets interpreter mind try: loadFile(arg) scan() registerLabels() parser() except Exception as e: print(fError: {e})这意味着同一进程可以连续解释多个汇编文件且文件之间互不影响状态。第一个程序Hello World仓库在 Assembler/examples/ 目录下提供了多个可直接运行的示例。最经典的 code2.txt 内容如下$msg db hello world mov ecx, $msg mov eax, 4 int 0x80 mov eax, 1 mov ebx, 0 int 0x80运行python assembler.py examples/code2.txt后控制台输出hello world END PROGRAM这段程序的逻辑与真实 Linux 汇编中的 write/exit 系统调用如出一辙先把字符串地址存入ecx再把系统调用号 4输出存入eax触发int 0x80随后以 1退出为调用号、ebx携带退出码 0再次触发int 0x80解释器打印END PROGRAM并终止。寄存器与算术指令语言提供四个寄存器与 Intel 32 位命名一致GUIDE.txt 中明确定义寄存器用途说明eax标准累加器mul/div默认使用ebx通用寄存器常作占位/传递退出码ecx通用寄存器系统调用 4输出时存放内容edx通用寄存器在 assembler.py 中四个寄存器被初始化为 Python 变量并预置初值注意eax初始为 1其余为 0eax 1 ebx 0 ecx 0 edx 0算术指令指令语法说明addadd p0, p1加法p0 p1subsub p0, p1减法p0 - p1mulmul [register]乘法结果存入 eaxdivdiv [register]除法结果存入 eax约束规则p0必须是寄存器p1可以是寄存器或常量mul与div隐式使用eax作为累加/被除数寄存器。看 GUIDE.txt 中的示例mov ecx, 56 sub ecx, 10 mov eax, 4 int 0x80执行流程第一行把 56 移入寄存器ecx第二行从ecx减去 10结果 46第三行把 4 移入eax为打印功能做准备第四行触发int 0x80结果打印到控制台之后需要换行继续README 强调每行必须以换行结尾int 0x80只执行当次调用。负数与十六进制常量从 scanner 的词法状态机可以看出解释器不仅支持十进制整数还支持负数状态 6 中允许-前缀与十六进制数状态 17/18 识别0x开头、由数字和a~f构成的字面量例如0x10。负数会被解析为负的浮点数存入寄存器十六进制字面量同样以值形式参与运算。系统功能中断 int 0x80通过int 0x80可以调用解释器内置的系统功能功能号由eax指定对应关系如下见 GUIDE.txt 的 System-Functions 章节EAX功能参数说明1退出程序错误码存放于 ebx3读取输入存入 ecx仅支持 float4控制台输出打印 ecx 中的内容这段逻辑对应 assembler.py 中int指令的处理分支if token.token 0x80: # system interrupt 0x80 if eax 1: # exit program if ebx 0: print(END PROGRAM) return else: print(END PROGRAM WITH ERRORS) return elif eax 3: ecx float(input( )) elif eax 4: # output information print(ecx)几个值得注意的实现细节退出码语义eax 1时若ebx 0打印END PROGRAM否则打印END PROGRAM WITH ERRORS——这模拟了 Linux 的进程退出码约定0 表示正常结束输入限制eax 3通过float(input( ))读取输入因此只接受可转为 float 的数值字符串输入会直接抛异常源码中标记为 TODO尚未处理 float 之外的响应输出内容eax 4直接print(ecx)因此要打印什么就先mov ecx, ...。变量与 db 指令变量以$前缀开头或全部使用大写字母书写GUIDE.txt 原文Variables begin with a $ or written in uppercase。定义变量使用db关键字其语法与 NASM 的dbdefine byte类似; variables VAR1 db 56 $var1 db 10 mov ecx, VAR1 mov ebx, $var1 sub ecx, ebx mov eax, 4 int 0x80运行结果46该示例对应 test.txt。GUIDE.txt 特别强调算术指令add、sub只接受寄存器或常量作为操作数不能直接对变量运算所以上面先用mov ebx, $var1把变量加载到寄存器ebx作为占位再执行sub ecx, ebx。源码层的变量处理在 parser 中db定义被解析进全局variables字典当遇到一个尚未登记的identifier后紧跟db关键字时后续的值/字符串会存入变量表数值转为 float字符串保持原样if tmpToken.t value or tmpToken.t string: if tmpToken.t value: variables[token.token] float(tmpToken.token) elif tmpToken.t string: variables[token.token] tmpToken.token而mov ecx, VAR1这类读取会在执行前查表替换若 token 是identifier且在variables中存在则直接用其值替代否则报错Error: Undefined variable ...见 assembler.py。因此变量本质上是一种编译期查表的符号替换机制。注意 README 的 hello world 示例中$msg db hello world存储的字符串正是通过这种查表机制在mov ecx, $msg时被替换为字符串本身再由系统调用 4 打印。注释注释以分号;开始直到该行结束。例如; begin program ; variable section $var1 db 56在 scanner 中状态 33 专门处理注释进入该状态后分号之后的字母、数字、非换行空白以及引号都被跳过直到遇到\n才回到初始状态注释内容不会产生任何 token。栈操作push / pop当需要暂存寄存器内容以防数据丢失时使用push与poppush eax把eax的内容压入栈顶而pop ecx把栈顶内容弹出并存入ecx。通用形式为push [register] pop [register]实现层面assembler.py栈是一个 Python 列表stack []push把寄存器名字符串追加进列表stack.append(token.token)pop则按目标寄存器取出栈顶值写回。值得一提的限制是pop 仅对四个寄存器生效且eax分支单独做了栈空检查打印Error: Stack Underflow而其他寄存器分支直接stack.pop()——从源码结构看这是实现上的不完整之处实际使用时应自行确保栈非空。code.txt 给出了 push/pop 与输入、加法组合的完整示例; begin program ; variable section $var1 db 56 $var2 db 44 mov ecx, $var1 push ecx mov eax, 3 int 0x80 pop eax add ecx, ebx add ecx, eax mov eax, 4 int 0x80程序先把$var156装入ecx并压栈保存随后通过系统调用 3 读取用户输入存入ecx再从栈中恢复原值到eax两次add累加后输出结果。比较与跳转cmp 与 je / jmpcmp指令用于比较两个寄存器cmp r0, r1 je l1 jmp l2若两寄存器相等jejump if equal生效并跳转到标签l1否则执行jmp无条件跳转到标签l2。实现机制cmp调用 setZeroFlag 比较两寄存器相等时把状态寄存器zeroFlag置为True源码注释称之为 status register见 assembler.py。此外add/sub执行后也会同步更新zeroFlag结果为 0 时置 True。je只在zeroFlag True时跳转jmp无条件跳转。标签Labels标签以字母l开头并包含数字定义时以冒号结尾l1:使用标签的注意事项定义标签时必须带冒号而在跳转指令中引用时不能带冒号。跳转表在正式解析前由 registerLabels 预构建它遍历所有 token把label与subprogram类型的 token 映射到其 token 索引存入jumps字典。这样jmp l1就能直接通过pointer jumps[token.token]把执行指针拨回标签位置见 assembler.py。由于跳转表是两遍扫描式的预注册标签可以出现在跳转语句之后无需前向声明。子程序call 与 ret子程序标签以_开头并以冒号结尾通过call调用且调用时省略冒号。每个子程序必须以ret结尾。code3.txt 给出了完整示例mov ecx, 5 call _double call _cube call _inc mov eax, 4 int 0x80 mov eax, 1 mov ebx, 0 int 0x80 _double: add ecx, ecx ret _cube: push eax mov eax, ecx add ecx, eax add ecx, eax pop eax ret _inc: add ecx, 1 ret执行逻辑ecx从 5 开始依次调用_double翻倍得 10、_cube立方得 30、_inc加一得 31最后输出 31 并退出。_cube内部先push eax保护现场运算完再pop eax恢复——这正是前面栈操作的实际应用场景。调用栈机制call/ret的实现assembler.py使用独立的returnStack列表call _name把当前执行指针压入returnStack然后跳转到子程序标签位置returnStack.append(pointer) # save the current pointer pointer jumps[token.token] # jump to the subprogramret从returnStack弹出返回地址写回指针若栈为空则报错Error: No return address on stack。由于指针是指向当前 token 的索引而子程序标签 token 本身会被parser空处理elif token.t subprogram: pass见 assembler.py跳入子程序后即从标签后的第一条指令开始顺序执行遇ret返回调用点继续。mul 与 div 的累加器语义code4.txt 演示了乘除法。mul/div与真实汇编一致隐式使用 eax 作为累加器mov eax, 8 mov ebx, 2 mul ebx mov ecx, eax mov eax, 4 int 0x80 mov eax, 8 mov ebx, 2 div ebx mov ecx, eax mov eax, 4 int 0x80乘法分支assembler.py对mul ebx执行eax * ebx对mul eax则执行eax * eax自乘除法分支assembler.py对div ebx执行eax / ebx。注意源码只在div ebx时检查了除数为 0 的情况打印Error: Division by Zero其他寄存器分支直接做浮点除法——这与 GUIDE.txt 中除法使用 eax的约定一致但除零防护并不完整实际使用时请自行确保除数非零。由于所有运算基于 Python floatdiv结果是浮点数而非整数截断。解释器的实现架构词法、符号表与执行器整个解释器采用经典的三段式结构理解它有助于你扩展或移植这门语言词法分析scan / scannerscanner是一个手写的状态机状态 0~50见 assembler.py逐字符扫描源码行将mov/add/sub/int/push/pop/jmp/je/cmp/call/ret/mul/div等识别为command寄存器识别为register数字含负数与十六进制识别为value字符串识别为string变量与大写标识符识别为identifierl*识别为label_*识别为subprogram。任何无法归类的字符组合都会抛出InvalidSyntax异常符号表预注册registerLabels在解释执行前先扫描一遍 token把标签和子程序登记进jumps跳转表实现先跳后定义解释执行parser以pointer为指令指针线性遍历 token 列表根据命令类型分支处理。这是核心执行器长度约占整个文件的一半。这种读文件 → 分词 → 建跳转表 → 顺序执行的架构使它很容易作为一个教学样例通过阅读 assembler.py 可以直观理解解释器与汇编器的差异——它不产生任何机器码而是直接在 Python 解释器中模拟寄存器、栈和中断行为。已知限制与注意事项基于源码与文档使用时需留意以下边界语法约束add/sub的第一操作数必须是寄存器第二操作数可为寄存器或常量不能直接写变量名参与运算需先mov到寄存器每行以换行结尾README 与 GUIDE 均强调此点解释器按行扫描缺少换行可能导致词法状态无法正确结束输入仅限 float系统调用 3 通过float(input())读取字符串输入会抛异常退出码eax 1且ebx 0打印END PROGRAM否则打印END PROGRAM WITH ERRORS除零检查不完整仅div ebx分支有防护pop 栈下溢防护不完整仅pop eax分支检查了栈空子程序必须 retcall依赖returnStack缺失ret会导致返回地址栈残留或错误直接对普通标签call会报Error: Unknown subprogram!。小结Python-Assembler 用一个单文件 Python 3 解释器复刻了 Intel 汇编语法的核心体验四个寄存器、mov/add/sub/mul/div算术指令、int 0x80系统中断输出/输入/退出、$与大写变量及db定义、;注释、push/pop栈操作、cmp/je/jmp条件与无条件跳转以及call/ret子程序机制。仓库自带的 examples 目录覆盖了上述全部特性可直接运行验证。对学习编译原理、解释器设计或汇编基础的开发者而言assembler.py 的状态机分词 符号表 指针驱动执行架构是一份清晰易懂的参考实现。【免费下载链接】PythonMy Python Examples项目地址: https://gitcode.com/gh_mirrors/py/Python创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考