Skip to content

汇编语言源程序格式

约 2006 个字 93 行代码 预计阅读时间 11 分钟

段的定义、假设与引用

段的定义

段定义的一般格式如下:

1
2
3
segmentname segment [use] [align] [combine] ['class']
    statements
segmentname ends

其中,方括号内部分是可选的。

  • 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,stack
    • public 用于代码段或数据段的定义,凡是段名、类别名都相同,且合并类型为 public 的段在连接时会合并成一个段
    • stack 用于堆栈段的定义,凡是段名、类别名都相同、且合并类型为 stack 的段在连接时会合并成一个堆栈段
    • 如果程序并不存在同名代码段或数据段,则可以省略 public ;但是堆栈段合并类型不能省略

段的假设

汇编指示语句 assume 可以用来建立编译器所需要的段与段寄存器的关联,其格式如下:

assume segreg:segmentname

segreg 可以为四个段寄存器其中一个,一般使用:

assume cs:code, ds:data, es:extra, ss:stk

assume 建立段地址和段寄存器的关联并不代表对段寄存器的赋值,而是帮助编译器在编译源程序时把变量或标号的段地址替换成关联的段寄存器,例如:

1
2
3
4
;假设变量 abc 位于 data 段偏移地址为 3 处
   mov ah, [abc]
=> mov ah, data:[3] ; 段地址不能用常数表示
=> mov ah, ds:[3]   ; After Translation

但是段寄存器 DS 和 ES 在程序开始时不会被赋值为首段的段地址,而是被赋值为 PSP 段址,因此一般在代码开始做如下赋值:

mov ax, data
mov ds, ax

堆栈段只能创建一次,创建时,ss:[sp] 指向栈顶,sp 指向堆栈段的末尾(开辟一段空间,里面的'S'只是用来占位的)

stk segment stack ; 开辟一段堆栈空间,里面的'S'只是用来占位的
a db 200h dup('S')
stk ends

----

+-------------+ <- ss:[sp], sp = 堆栈长度
|stack segment|
+-------------+ <- ss:[sp - 200h] or ss
|code  segment|
+-------------+ <- cs:[ip], ip = offset main
|data  segment|
+-------------+ <- ds

常数与常数表达式

汇编语言支持的常数包括整型常数、浮点型常数、字符常数、字符串常数,如:

1
2
3
4
5
6
10110000B       ; 二进制,B为后缀
177Q, 17777777Q ; 八进制,Q为后缀

y dq 1.6E-307   ; double 类型浮点数,采用科学计数法

s db 'C','I','A','L','L','O'  ; 等价 s db "CIALLO"
运算符 格式 含义
+ 正 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 取偏移地址

常数表达式中只能包含运算符和常数,不能含有寄存器或变量名

另外,汇编语言还支持定义符号常数(类似宏),如:

char = 'A'
exitfun equ <mov ah, 4Ch>
dosint  equ <int 21h>
code segment
assume cs:code,
main:
    mov ah, 2
    mov dl, char
    dosint
    char = 'B'
    mov ah, 2
    mov dl, char
    dosint
    exitfun
    dosint
code ends
end main
  • = 的操作数只能是数值类型或字符类型的常数或常数表达式,且可以对该符号多次定义
  • 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 指令的目标地址。定义标号的一般格式如下:

lablename:

除了用标号名加冒号的形式定义标号外,我们还可以用伪指令 label 来定义标号,其格式如下:

labelname label near|far|byte|word|dword|qword|tbyte

其中 near,far 是用于跳转的标号类型,其它五个则是变量类型。

定义一个标号为近标号还是远标号取决于以该标号为目标地址的跳转指令是否与其在同一个段中。近标号和远标号在编译后都转化成地址,其中近标号转化为其在该段中的偏移地址,而远标号转化为段地址:偏移地址。因此,如果跳转指令要引用别的段的标号,那么该标号应被定义为远标号,而指令引用它时要加上 far ptr 强制把标号编译成完整地址,如:

code segment:
assume cs:code, ds:data
    ...statements...
    jmp far ptr far_away; 跳转到远标号 far_away
far_back label far      ; 定义 far_back 为远标号
    add al, abc[0]
    mov ah, 4Ch
    int 21h
    ...statements
main:
    ...statements
code ends

away segment
assume cs:code, ds:data
far_away label far      ; 定义 far_away 为远标号
    mov al, abc[3]
    jmp far ptr far_back; 跳转到远标号 far_back
away ends
end main

当跳转指令向后引用远标号时,可以将 far ptr 省略,如上面的 jmp far_away

假设有一变量名 var,则在代码段中既可以用 var 又可以用 [var] 来引用该变量;假设有一 db 类型变量数组 a ,则在代码段中既可以用 a[1] 又可以用 [a+1] 开引用该数组的第一个元素。

在数据段中,varoffset var 都可作为伪指令 dw 的操作数表示变量 var 的偏移地址,也可以将 var 作为 dd 的操作数表示变量 var 的偏移地址、段地址;但是在代码段中只能用 offset var 来引用其偏移地址,用 seg var 或数据段名来引用其段地址:

1
2
3
4
data segment:
xyz db 'W'
addr1 dw offset xyz ; 也可以写成 addr1 dw xyz
addr2 dd xyz        ; 也可以写成 addr2 dw offset xyz, seg xyz
位置计数器

在段定义开始时,编译器自动将位置计数器清零,然后每编译完一条指令或伪指令语句,编译器会把该指令语句字节码的字节数加到位置计数器中。在汇编程序中,可以通过 $ 来获取当前位置的位置计数器的值。

过程调用

通过堆栈传递参数,在 call 指令前将参数按照从右到左的顺序压入栈(对应的栈从上到下),然后先在函数开头执行 push bp, mov bp, sp 获取当前状态栈顶地址存入 bp 。由于 call 指令执行过程中会将返回地址也压入堆栈,因此实际调用参数要从 [bp+4] 开始(also ss:[bp+4]):

code segment
assume cs:code
f:  ; f(int a, int b) return a+b
    push bp
    mov bp, sp
    mov ax, [bp+4]  ; a
    add ax, [bp+6]  ; b
    pop bp          ; 恢复 bp
    ret   ; return 指令同样会 pop 栈找出返回地址

main:
    mov ax, 3
    push ax
    mov ax, 2
    push ax
    call f  ; call 指令会将下一条指令的地址压入栈
back:
    add sp, 4  ; 清空栈
code ends
end main

如果函数有返回值,一般根据位数使用 al,ax,eax 来存储返回值。

How to return 64 bit?

使用 edx:eax 分别存储高位和低位

在函数调用前后,依照不同的规则需要保证一些寄存器的前后值不变,因此我们需要在函数开头和结尾保存它们的状态并恢复。

中断的调用其实相当于内置函数的调用,中断函数的地址存储在内存 0000:0000 后,每四个字节表示一条中断指令。例如,对于如下所示的一段内存:

1
2
3
4
5
6
7
8
0000:0054:  40h
0000:0055:  02h
0000:0056:  58h
0000:0057:  02h
0000:0058:  2Dh   <- int 16h 中断向量起始地址
0000:0059:  04h
0000:005A:  70h
0000:005B:  00h

对于指令 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=返回码
Comments: