Delphi BASM精要:从寄存器寻址到调用约定的底层汇编指南

发布时间:2026/10/10 15:58:30

Delphi BASM精要:从寄存器寻址到调用约定的底层汇编指南 简介面向 Delphi 开发者的一份 BASM 精要资料特别适合想在 Object Pascal 中嵌入汇编、为关键逻辑提速的读者。资料从汇编语句的构成与表达式类别讲起先说明零至三个表达式如何组成一条指令再分别梳理寄存器、立即数、内存引用三类操作数并指出每个表达式必须能在编译期求出确定值的约束。随后用大量示例演示 Delphi 变量、常量、记录、数组在 BASM 中的访问方式包括通过基址加偏移读取字段、数组与字符串字节内容覆盖 DB/DW/DD/DQ 四种数据定义指令和表达式.类型、类型(表达式)两种类型强制转换写法对常量作为立即数编码、OFFSET 失去意义、TYPE 返回类型长度、数组下标不可直接访问等易混淆点也给出明确说明。整套内容仅含 1 个 PDF大小约 30KB轻量便携。资源目前已有 131 人学习适合刚接触 Delphi 内嵌汇编的开发者快速建立 BASM 概念、避开典型陷阱。1. 一份 BASM 精要文档为什么搞 Delphi 底层的人离不开这一页纸做底层软件开发的从业者迟早会在调试器的反汇编窗格和汇编代码正面相遇。定位第三方组件的崩溃栈、反向梳理某段生成代码、或想把几条高频指令用手写汇编替换进 Delphi 源码时绕不开 BASMBorland 汇编语言。这份 PDF 不铺开讲整本汇编教材只挑 BASM 在 Delphi 环境里真正会碰到的语法边界表达式分类、数据定义、调用约定、完全汇编例程与内嵌汇编例程的差异。适合两类人——一类是刚在 Delphi 里看见asm..end想弄清楚这块语法是怎么运作的另一类是已经在读反汇编却被调用约定、变量寻址这些问题反复卡住的人。下面按使用顺序把这几个主题逐一拆开。2. 表达式的三个类别与寻址差异寄存器、立即数、内存引用怎么影响指令编码2.1 表达式类别决定编译器生成什么指令BASM 的每一条汇编语句由指令加上零到三个表达式构成。表达式虽然写法多样但按计算结果只能归入三类寄存器、立即数、内存引用。这个分类不是概念游戏它直接影响指令编码方式和执行效率。寄存器表达式add eax, 1操作数直接引用寄存器指令短且快。立即数表达式mov eax, 100数值被直接编码进指令码流指令本身携带数据不需要访存。内存引用表达式imul edx, [ebx].RandSeed, 08088405H操作数指向内存地址执行时需要先经过地址计算再读写内存指令编码更长。实际写底层代码时这三类的取舍会直接影响运行效率。同一个寄存器在指令中只需一个字节段即可表达而内存引用通常要多出寻址模式字节和偏移量。比如遍历一个字符数组反复用[ebxesi]这种基址加变址的方式取数和先把内存值加载进寄存器再用寄存器运算两种情况生成的指令条数相差很大。Delphi 编译器对高级代码做优化时经常做这种选择BASM 手写代码时同样要在可读性和指令效率之间权衡。TYPE 是 BASM 里一个容易被忽略的关键字返回的是表达式类型占用的字节数语义和 Delphi 的SizeOf()一致。它不会返回元素类型的大小而是返回完整表达式的长度。看一个典型定义type TArr array [0..10] of char; // SizeOf(TArr) 11在这个例子中TYPE TArr的返回值是 11。很多人在 BASM 里试图用数组下标语法访问元素却得到完全不符合预期的行为根源就在这里——Arr[2]这整个表达式被当成一个类型长度为 11 的对象而不是数组中的单个字符元素。理解了这一层后面很多编译报错就能对号入座。2.2 变量、常量、记录与 OFFSET 的边界BASM 可以访问 Delphi 定义的变量和常量但访问路径需要区分清楚。看下面这组代码asm mov eax, I // I 的值送入 eax mov eax, [I] // 同上变量访问的等价写法 mov eax, OFFSET I // I 的地址送入 eax等价于 I mov eax, R.rI // 记录字段 rI 的值送入 eax mov eax, [TRec.rI R] // 同上一行通过块基址加字段偏移寻址 end;第一行和第二行是等价的。BASM 里mov eax, I已经把变量 I 当成内存引用处理与[I]没有区别。所以一个容易混的点在于OFFSET才是取地址的显式写法它在编译器眼里是静态地址运算符只能用于编译期可以确定地址的对象。常量在这个规则下是另一类东西。常量不占用运行时内存编译器会把值直接编码进指令。参照下面例子const C 3124; asm mov eax, C // eax 3124C 作为立即数 mov eax, [C] // 访问内存地址 3124非法访问 end;mov eax, C中 C 被当作立即数 3124 编码进指令。而[C]表达的是访问内存地址 3124这是毫无意义的非法操作。同理对常量使用OFFSET也没有意义因为常量没有可供引用的内存地址。字符串常量则是另一个特殊案例BASM 在处理短字符串常量时如果长度不大于 4可以整体送入寄存器。ab会得到$00006162左侧补零abcd得到$61626364。长度超过 4 则无法整体进入寄存器必须走地址运算按字节访问这是许多想直接搬字符串进 EAX 的人遇到的第一个坑。2.3 数组访问地址运算替代下标语法BASM 的语法并不直接支持数组下标。mov eax, TYPE Arr[2]这样的语句虽然能通过编译但返回的是整个数组的类型长度而不是预期元素的值。同样的道理mov al, Arr[2]编译不过——把 11 字节的对象送入 al 寄存器显然不合法。BASM 里对数组元素的访问要用地址运算来完成var A: array [0..10] of char abcdefghij#0; asm mov ebx, OFFSET A // ebx 指向数组首地址 mov eax, [ebx4] // 读取 A[4] 到 A[7] 四个字节作为 DWORD end;[ebx4]计算出数组首地址加 4 个字节的内存地址一次读出 4 字节。字符串变量在 Delphi 里的存储格式也遵循这个规律字符串变量本身是一个指针指向的内容开头有长度字节。访问字符串内字符时常见的做法是先把指针送入寄存器减去长度区再偏移到目标下标var S: String 1234567; asm mov ebx, S // String 是 4 字节指针ebx 指向字符串数据 dec ebx // 跳过长度字节 mov al, BYTE [ebx4] // 取 s[4] 的字符值 end;看到这里应该能理解为什么 BASM 处理数组和字符串总显得绕它把高级数据结构还原成纯内存布局要访问哪个字节自己算偏移。3. 五种调用约定与参数传递从 register 到 stdcall 的选型依据3.1 五种调用约定传参顺序、栈平衡责任、适用场景BASM 里调用例程前必须搞清楚调用约定因为这决定参数从哪边开始压栈、由谁清理栈、哪些参数走寄存器。不同约定混用的后果通常是栈不平衡或参数的寄存器分布错位。五种约定对比如下调用约定传参顺序清除参数责任寄存器传参适用场景register由左至右例程自身是Delphi 默认规则公开声明必须使用pascal由左至右例程自身否兼容旧代码较少使用cdecl由右至左调用者否与 C/C 模块交互stdcall由右至左例程自身否Windows API最常用safecall由右至左例程自身否COM 双重接口、异常处理register 是 Delphi 默认约定效率最高前几个参数在寄存器里直接传递不需要碰栈。stdcall 是 Windows API 的标准约定参数从右往左压入栈由例程自身清栈。cdecl 则是调用者负责清栈混用时最容易出现栈泄漏。实际项目里最常见的组合是Delphi 内部的函数用 register调用系统 API 用 stdcall跨语言模块用 cdecl。搞混 stdcall 和 cdecl 是老问题——call返回后如果没有恢复栈指针程序几乎立刻崩溃。3.2 register 约定调用 Delphi 函数局部变量与全局变量的寻址差异按 register 约定调用 Delphi 函数时参数优先占用 EAX、EDX、ECX超过三个才会压栈。看下面这段完整的调用代码function DelphiFunc(I: Integer; var S1, S2: String): Integer; begin if I Length(S1) then SetLength(S1, I); S1 : S1 S2; Result : Length(S1); end; var GS: String 12345678; procedure RegisterCall; var LS: String; Len: Integer; begin LS : This is a test!; asm mov eax, 8 // 第一个参数 I值直接进 eax lea edx, LS // 局部变量必须用 lea 取地址 mov ecx, OFFSET GS // 全局变量用 OFFSET 是保留字转义符 call DelphiFunc mov Len, eax // 返回值在 eax end; Writeln(LS); Writeln(Len); end;这里有两个关键点。第一局部变量 LS 在栈上地址要由 EBP 在运行时算出来所以必须用lea edx, LS装载地址不能写成mov edx, OFFSET LS——后者拿到的静态地址不是局部变量在栈上的真实位置。第二全局变量 GS 的地址在编译期就是确定的可以用OFFSET。这里 GS 恰好与段地址寄存器 GS 同名所以用复写标识符转义为变量写成OFFSET GS。3.3 stdcall 约定调用 Windows API参数入栈顺序与 Result调用 Windows API 时要严格按 stdcall 的处理方式参数从右往左逐个压栈例程负责清理。下面这段演示如何用 BASM 调起 kernel32 的 GetFileSizefunction GetFileSize(Handle: Integer; x: Integer): Integer; stdcall; external kernel32.dll name GetFileSize; function stdcallDemo: Integer; var FH: THandle; begin FH : FileOpen(C:\boot.ini, fmOpenRead); asm push 0 // 第二个参数 nil 入栈参数从右往左压 push FH // 第一个参数入栈 call GetFileSize mov Result, eax // 返回值在 eax写入 Delphi 函数的返回值 end; FileClose(FH); end;注意push的顺序参数按从右到左的顺序入栈所以先压第二个参数 nil再压第一个参数。因为 stdcall 由例程自身恢复栈调用结束后不需要手动add esp, 8这一点与 cdecl 有明显区别。Result是 BASM 提供的专用返回值占位用它写回 Delphi 函数返回值是标准做法。还要提醒一点返回值只在 EAX 中大于 32 位的返回值比如 Int64 或浮点数不在这个范围内这是 BASM 手写调用的一个已知边界。参数传递的基本规则值得单独记一下值参数和 const 参数在数据长度小于等于 4 字节Int64 除外时直接传值否则传内存地址var 参数和 out 参数总是传地址。完全汇编例程中值参数与 const 参数处理一致这一点在第 5 章避坑部分还会再展开。4. 在代码段里定义数据DB/DW/DD/DQ 与类型强制转换的边界4.1 用跳转指令和标号在代码段里安放数据BASM 不提供像传统汇编那样为数据命名并分配独立数据段的能力但它保留了 DB、DW、DD、DQ 四条定义数据的指令。一个常见的使用模式是段首先放一条jmp跳过数据区接着用 DB/DD 定义数据数据末尾放一个标号再回到真正的执行代码。下面的例子展示了从例程名和标号两种方式读取代码段里的自定义数据type TCode packed record CODE: Word; // jmp 的指令占位2 字节 I: Integer; // 4 字节 S1: array[1..26] of char; S2: array[1..11] of byte; end; var I: Integer; S: String; Code: ^TCode; function ReadCode: Integer; asm jmp DD 12344213 DB ABCDEFGHIJKLMJNOQRSTUVWXYZ DB 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32 : mov Code, offset ReadCode mov EAX, ReadCode.TCode.I end;执行过程是先jmp 跳到数据之后数据区被直接跳过。而mov Code, offset ReadCode取得例程基地址之后按 TCode 记录布局访问 I 字段读到值 12344213。BASM 不允许为这些数据命名想引用它们只能靠例程名或标号。更简洁的写法是直接把标号当变量名用function ReadCode: Integer; asm jmp CodeRec: DD 12344213 : mov EAX, CodeRec.TCode.I end;这里CodeRec是标号CodeRec.TCode.I表示从该标号地址偏移到记录字段 I。用这种方式定义的数据存放在代码段这也弥补了 Delphi 不能在代码段定义数据的空缺。但要注意代码段在默认情况下是只读的如果要写这些数据需要先通过操作系统 API 放开代码页的写权限否则一写就访问违规。4.2 类型强制转换重新解释地址不做长度校验BASM 支持两种类型强制转换语法效果完全一致表达式.类型和类型(表达式)。type TCode record I: Integer; S: String; end; var aRec: TCode; aInt: Integer; asm mov eax, aInt.TCode.I // 表达式.类型的格式 mov eax, integer(aRec) // 类型(表达式)的格式 end;两种写法语义相同都是把变量地址上的字节按目标类型重新解释。但与 Delphi 高级语法里的强制转换不同BASM 的强制转换不做任何长度校验。TCode 记录长度是 8 字节Integer 长度 4 字节两者照样可以互相转换。在 Delphi 代码里 8 字节转 4 字节会直接报错BASM 里却通行无阻。这种不校验的特性在做内存布局多态解析时非常有用但也意味着转换是否安全完全取决于写代码的人对内存布局的把控用错就是越界读。4.3 两个容易踩的边界立即数常量的地址无意义常量在 BASM 中被直接编码取地址运算符对它无效。之前提过mov eax, C执行后 EAX 得到 3124但mov eax, OFFSET C在多数情况下也能被编译器接受实际生成的结果却不是你想要的——它把常量值本身当作立即数编码进指令OFFSET在这里等于不存在。即使在 Delphi 层字符串常量带内存地址BASM 仍然不提供访问路径。一个实际影响是需要把某段固定字节序列放进寄存器时长度超过 4 就必须改用地址访问。短字符串可以直接利用立即数长字符串要么用数据定义指令在代码段里预置字节序列要么从 Delphi 全局变量读取。区分好值是立即数和值是内存引用这两类表达方式BASM 里一半以上的编译报错都能定位。5. 完全汇编与流程控制JMP/CALL 的语义差异与高频排查记录5.1 JMP 与 CALL 的本质区别错误处理为什么偏爱 JMP在 BASM 里JMP 和 CALL 看起来都是转移控制流但两者有一个本质区别CALL 会把返回地址压栈JMP 不会。JMP 跳到目标例程后目标例程执行到 RET 时从栈顶弹出的返回地址是 JMP 之前的入栈值而非 JMP 所在的下一行。System.pas 里处理纯虚方法错误的逻辑就是一个典型场景NoAbstErrProc: MOV EAX, 210 JMP _RunError运行到这里时JMP _RunError让_RunError例程直接接管当前例程的 RET。错误处理完成后直接返回到最初调用者的下一条指令出错点后面的代码全部跳过。如果改用 CALL错误处理例程 RET 时会回到这里继续执行出错点后方的指令显然不是期望行为。在异常处理、清理逻辑这类出错后不应继续执行的场景里JMP 是正确选择。5.2 让 JMP 返回到下一行DB $E8 序列的字节级拆解有时候需要在跳转后让控制流回到下一行继续执行常规 JMP 做不到。BASM 里可以通过手工构造的机器码序列改写 EIP 实现。下面这段字节码在任何位置插入都能达到效果DB $E8, $0, $0, $0, $0, $8F, $04, $24, $83, $04, $24, $0C jmp proc逐字节拆开看$E8 00000000call GetEIP5 字节将下一条指令地址压入栈$8F 04 24pop [esp]3 字节弹出的值写回栈顶$83 04 24 0Cadd [esp], 124 字节给栈顶值加 12jmp proc远程无条件跳转5 字节执行完add [esp], 12后栈顶的值被修改为jmp proc之后那一行的地址。目标例程执行 RET 时弹出这个地址于是控制流返回到proc的下一行效果等同于 CALL。这里加 12 是因为从call GetEIP结束到jmp proc结束的指令长度合计是 34512 字节。这套技巧在需要动态分发、又不想破坏堆栈结构时很有用但对字节偏移的敏感度极高指令长度一变加的值就要跟着调整。5.3 完全汇编例程与内嵌汇编例程值参数复制的差别BASM 在例程中使用有两种形态。完全汇编例程是用asm替换例程的begin整个例程体由汇编构成内嵌汇编例程则是在begin..end之间某处插入asm..end块。两者最关键的差异在于完全汇编例程没有begin编译器不会生成值参数的复制代码。值参数和 const 参数在完全汇编例程里按同一规则处理长度不超过 4 字节直接传值否则传地址。编译器通常会自动处理完全汇编例程的栈结构。但如果例程不是嵌套子例程、没有入口参数或局部变量编译器不会在栈上分配空间。也就是说这种最简例程既没有栈帧也没有局部变量区访问局部变量这类操作在编译阶段就会被判定为非法。内嵌汇编例程则不同它在 Delphi 的 begin..end 保护伞下先有完整的高级语言栈帧再在其中插入汇编逻辑堆栈结构完全由编译器管理。实际项目中如果需要精细控制每一条指令的栈行为用完全汇编例程如果只是在已有函数里加速一小段计算内嵌汇编例程更稳妥。5.4 四个高频踩坑与排查记录一、调用 Delphi 函数后 EDX、ESI 的值变了。原因BASM 允许任意使用 EAX、ECX、EDX 三个寄存器但必须保持 EDI、ESI、ESP、EBP、EBX 不变。编译器生成的函数内部可能使用这些寄存器并自动保存恢复但完全汇编例程不会。解决在汇编块里如果必须临时使用 ESI/EDI先入栈保护退出前恢复。二、局部字符串内容在汇编调用后变成乱码。原因局部变量在栈上引用时必须用lea edx, LS而不是mov edx, OFFSET LS。OFFSET 是编译期静态地址运算符不能动态计算栈上地址。解决统一用lea装载局部变量的地址只有全局变量、常量才可以用OFFSET。三、mov al, Arr[2]编译失败。原因BASM 不把Arr[2]当数组元素处理它把整个表达式看成类型长度为 11 的数组对象无法匹配 al 寄存器宽度。解决改为地址运算先mov ebx, OFFSET Arr再mov al, [ebx2]。四、完全汇编例程返回值不正确。原因完全汇编例程不做值参数备份值参数直接指向调用者的内存函数内对参数的修改可能泄漏到外部。解决确认是否需要保留参数原始值需要的在例程开头用栈或寄存器暂存。注意这些坑有一个共同规律——BASM 是直接面对内存布局的语法层编译器不会替你兜底类型长度、栈地址、寄存器保护。每条报错都值得先问一句这个表达式在编译器眼里到底是什么类型、什么长度。6. 验证 BASM 写法的三板斧CPU 窗口、寄存器追踪与最小的自检函数汇编代码写对了没有编译通过只是第一步运行时行为才是硬标准。我验证 BASM 代码通常固定走三个步骤。先在调试器里打开 CPU 窗口。在 asm 块命中断点后切到反汇编视图核对指令的编码形态。同样是mov eax, I如果窗口里显示的是取 DWORD 内存引用说明变量访问路径正确如果显示的是立即数装载那就说明编译器没有把 I 当作内存引用处理和预期的寻址模式不一致这种差异在高级代码里看不出来在反汇编视图里一眼就能发现。第二个方法是寄存器追踪。写一个最小化的完全汇编函数只做最简单的运算并返回结果再用 Delphi 代码调用并打印function AddOne(X: Integer): Integer; asm mov eax, X inc eax end; // 调用 Writeln(AddOne(41)); // 期望输出 42这个最小用例能验证完全汇编例程的入口参数装载、返回值写回、调用约定三件事是否同时正确。如果输出不是 42问题要么出在参数传递要么出在Result的使用方式。基础用例通过后再往里填复杂逻辑复杂度越高越容易定位错误源。第三种做法是检查返回值的写入方式。完全汇编例程里返回值使用Result它由 BASM 定义指向真实返回槽内嵌汇编例程在 begin..end 之间Result是 Delphi 局部变量其存储位置可能已分配给某个寄存器。两种形态下写入返回值的方式完全不同用错必然导致函数返回垃圾值。我最初踩过这个坑写的内嵌汇编块里用了Result结果编译器把它当成普通标识符返回值和预期完全对不上。从那以后我每次写完 BASM 代码都强制走三遍先核对 CPU 窗口里的指令形态再跑最小函数验证参数和返回值通道最后确认Result与Result用对场合。这份 PDF 最大的价值不是教程式的概念罗列而是把这些边界和坑点集中摆在了一起。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/10 15:58:30

从零打造Linux无线热点:hostapd配置实战与排错指南

聊到把一台普通Linux设备变成无线热点,绕不开的工具就是hostapd。我在实际项目里用它把一台旧笔记本和一张USB无线网卡改造成了机房临时调试用的接入点,说实话,这个工具配置起来不算难,但坑确实不少,尤其是初学者容易卡…

2026/10/10 15:58:30

hello-algo 分数背包问题:贪心策略、代码实现与正确性证明

教程文档示例工程教育 【免费下载链接】hello-algo 《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語,提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现 项目地址&#x…

2026/10/10 21:30:52

C# OpenCvSharp + ONNX Runtime 实现L2CS-Net人脸注视与朝向估计

简介:采用C#与OpenCvSharp实现的L2CS-Net本地推理方案,专为需要在桌面端完成眼睛注视方向或人脸朝向估计的开发者准备。基于WinForm界面与ONNX Runtime加载模型,可离线运行,适合人机交互、疲劳监测、视线追踪等应用场景的快速验证…

2026/10/10 21:30:52

让STK11驱动多智能体强化学习:卫星调度全流程实践

简介:一份基于Python与STK11的多智能体强化学习卫星调度实验资源,面向希望入门强化学习与卫星任务规划的学习者,也适合作为毕设、课程设计或工程实训项目。资源包含完整的任务生成与访问时段计算流程:mission.py定义随机任务属性&…

2026/10/10 21:30:52

YOLOv8实战:热轧带钢表面缺陷检测从数据到部署全流程

简介:面向深度学习和工业质检开发者,这份资源聚焦基于YOLOv8的热轧带钢表面缺陷检测,覆盖横向裂缝、纵向裂缝、坑槽等八类缺陷的识别。资源属于软件/插件与数据集结合型,适合想要快速上手目标检测项目或落地产线质检的读者。包体共…

2026/10/10 21:30:52

马行为识别数据集:从VOC解析到YOLO训练全流程

简介:马行为识别数据集是一套面向计算机视觉与深度学习场景的标注资源,主要用于马匹行为自动识别,覆盖站立、吃草、躺下等常见动作,对应不同的标注类别,整体识别准确率约为89.8%。压缩包共2000个文件,均为P…

2026/10/10 7:31:36

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/9 20:15:56

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 0:04:53

从逻辑门到计算机:数字电路核心原理与全加器搭建实战

如果你拆过一台旧电脑的主板,盯着那些黑乎乎的小芯片看上一会儿,可能会冒出同一个疑问:这堆引脚密集的元件,到底是怎么“变”出那么复杂的应用的?答案并不在某个神秘的部件里,而是在所有芯片内部都在反复使…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑