汇编语言源程序格式
约 2006 个字 93 行代码 预计阅读时间 11 分钟
段的定义、假设与引用¶
段的定义¶
段定义的一般格式如下:
其中,方括号内部分是可选的。
use表示段内偏移地址宽度,可选择use16,use32- 若汇编程序开头有
.386,则默认使用use32,不想要的话需要手动加上use16
- 若汇编程序开头有
align表示对其方式,可选择byte,word,dword,para,page,用来规定该段的边界的对齐方式,即段首地址能被align整除para是 16 字节,page是 256 字节,默认为para- 如果前一个段末尾地址不能被该段
align整除,连接器会以 00h 对前一个段末尾进行填充。一般默认只要段首地址满足十六进制最低位为0即可。
class表示类别名,必须用单引号括起来- 具有相同类别名的段在连接时会被重新安排顺序使其在 EXE 程序中邻近
combine表示合并类型,可选择public,stackpublic用于代码段或数据段的定义,凡是段名、类别名都相同,且合并类型为public的段在连接时会合并成一个段stack用于堆栈段的定义,凡是段名、类别名都相同、且合并类型为stack的段在连接时会合并成一个堆栈段- 如果程序并不存在同名代码段或数据段,则可以省略
public;但是堆栈段合并类型不能省略
段的假设¶
汇编指示语句 assume 可以用来建立编译器所需要的段与段寄存器的关联,其格式如下:
segreg 可以为四个段寄存器其中一个,一般使用:
用 assume 建立段地址和段寄存器的关联并不代表对段寄存器的赋值,而是帮助编译器在编译源程序时把变量或标号的段地址替换成关联的段寄存器,例如:
但是段寄存器 DS 和 ES 在程序开始时不会被赋值为首段的段地址,而是被赋值为 PSP 段址,因此一般在代码开始做如下赋值:
堆栈段只能创建一次,创建时,ss:[sp] 指向栈顶,sp 指向堆栈段的末尾(开辟一段空间,里面的'S'只是用来占位的)
常数与常数表达式¶
汇编语言支持的常数包括整型常数、浮点型常数、字符常数、字符串常数,如:
| 运算符 | 格式 | 含义 |
|---|---|---|
| + | 略 | 正 or 加 |
| - | 略 | 负 or 减 |
| * | x * y | 乘 |
| / | x / y | 除 |
| MOD | x MOD y | 取余 |
| SHR | x SHR y | x 右移 y 位 |
| SHL | x SHL y | x 左移 y 位 |
| NOT | NOT x | 求反 |
| AND | x AND y | 求与 |
| OR | x OR y | 求或 |
| XOR | x XOR y | 求异或 |
| SEG | SEG x | 取段地址 |
| OFFSET | OFFSET x | 取偏移地址 |
常数表达式中只能包含运算符和常数,不能含有寄存器或变量名
另外,汇编语言还支持定义符号常数(类似宏),如:
- = 的操作数只能是数值类型或字符类型的常数或常数表达式,且可以对该符号多次定义
- EQU 的操作数除了数值类型或字符类型的常数或常数表达式外,还可以是字符串,甚至是一条汇编语句,但不可多次定义
变量与标号¶
变量名和标号名可从 0-9a-zA-Z@?$_ 中选取,其要求如下:
- 变量名和符号名不能以数字开头
$和?不能单独用作变量名和符号名- 变量名和符号名所包含字符不能超过 31 个
- 一般情况,变量名和标号名都不区分大小写
请指出以下变量名中哪些是不正确的,并说明错误原因
- (1) aaa ; 正确
- (2) bbb ; 正确
- (3) VIDEO-3D ; 不正确。变量名中不能含有减号。
- (4) It_is_ok! ; 不正确。变量名中不能含有感叹号。
- (5) ?IsItRight ; 正确
- (6) 2Small ; 不正确。变量名不能用数字开头。
- (7) MP2$MP3@MP4 ; 正确
- (8) FFFFh ; 正确
标号时符号形式表示的跳转目标地址,标号既可以用作跳转指令,如 jmp,jnz,loop 的目标地址,也可以用作 call 指令的目标地址。定义标号的一般格式如下:
除了用标号名加冒号的形式定义标号外,我们还可以用伪指令 label 来定义标号,其格式如下:
其中 near,far 是用于跳转的标号类型,其它五个则是变量类型。
定义一个标号为近标号还是远标号取决于以该标号为目标地址的跳转指令是否与其在同一个段中。近标号和远标号在编译后都转化成地址,其中近标号转化为其在该段中的偏移地址,而远标号转化为段地址:偏移地址。因此,如果跳转指令要引用别的段的标号,那么该标号应被定义为远标号,而指令引用它时要加上 far ptr 强制把标号编译成完整地址,如:
当跳转指令向后引用远标号时,可以将 far ptr 省略,如上面的 jmp far_away
假设有一变量名 var,则在代码段中既可以用 var 又可以用 [var] 来引用该变量;假设有一 db 类型变量数组 a ,则在代码段中既可以用 a[1] 又可以用 [a+1] 开引用该数组的第一个元素。
在数据段中,var 或 offset var 都可作为伪指令 dw 的操作数表示变量 var 的偏移地址,也可以将 var 作为 dd 的操作数表示变量 var 的偏移地址、段地址;但是在代码段中只能用 offset var 来引用其偏移地址,用 seg var 或数据段名来引用其段地址:
位置计数器
在段定义开始时,编译器自动将位置计数器清零,然后每编译完一条指令或伪指令语句,编译器会把该指令语句字节码的字节数加到位置计数器中。在汇编程序中,可以通过 $ 来获取当前位置的位置计数器的值。
过程调用¶
通过堆栈传递参数,在 call 指令前将参数按照从右到左的顺序压入栈(对应的栈从上到下),然后先在函数开头执行 push bp, mov bp, sp 获取当前状态栈顶地址存入 bp 。由于 call 指令执行过程中会将返回地址也压入堆栈,因此实际调用参数要从 [bp+4] 开始(also ss:[bp+4]):
如果函数有返回值,一般根据位数使用 al,ax,eax 来存储返回值。
How to return 64 bit?
使用 edx:eax 分别存储高位和低位
在函数调用前后,依照不同的规则需要保证一些寄存器的前后值不变,因此我们需要在函数开头和结尾保存它们的状态并恢复。
中断的调用其实相当于内置函数的调用,中断函数的地址存储在内存 0000:0000 后,每四个字节表示一条中断指令。例如,对于如下所示的一段内存:
对于指令 int 16h ,其中断向量位于 0000:16h*4 ,即起始地址为 0000:0058 ,并且按照小端存储规则,当我们调用中断指令 int 16h 时,下一条指令的地址应为 0070:042D,这时 CS=0070h, IP=042Dh。
int 21h 的常用中断:
| AH | 功能 | 补充解释 |
|---|---|---|
| 01 | 有回显地读取一个标准输入 | AL=输入字符, 实际使用中还可以用作末尾敲任意键结束程序 |
| 02 | 输出字符到标准输出 | DL=输出字符 |
| 09 | 输出字符串到标准输出 | DS:DX=字符串地址,一直到 '$' 停止输出 |
| 4C | 带返回码终止 | AL=返回码 |