Part 5. 进阶内容
函数
定义
函数(function)又称过程(procedure)。在x86中,根据调用和返回指令的不同,有**近函数(过程)和远函数(过程)**之分。一共有2种定义函数的方式:
用标号定义函数(常用)
asm; 近函数定义1 标号名: ... retn ; 可简写为 ret ; 近函数定义2 标号名 label near ... retn ; 可简写为 ret ; 远函数定义 标号名 label far ... retf用
proc定义函数asm; 近函数定义 函数名 proc near ... retn ; 可简写为 ret 函数名 endp ; 远函数定义 函数名 proc far ... retf 函数名 endp
调用和返回
搭配使用call类指令和ret类指令,大致原理为(这里以近函数为例,具体功能和使用方法可参见Part 4对应部分):
- 函数调用:
call指令后面的指令的偏移地址(ip)被存在栈内,然后根据操作数(标号、寄存器等等)跳转到指定函数部分,执行指令 - 函数返回:在函数的最后使用
ret指令,获取栈内被保存的ip,跳转到call指令后一条指令的地址上,从而实现返回功能
参数和返回值的传递
函数传参的方式:
- 用寄存器传参
- 小技巧:如果批量传递连续的一组数据(比如字符串),此时不需要传递完整的字符串,只需要传递这组数据的首地址(即第一个元素的地址),以及这组数据的长度即可(
很像C语言的处理) - 局限:寄存器数量较少,可能不够用,而且还可能存在(调用者与被调用者之间的)冲突
- 避免冲突的方法:在函数正式执行前,先将函数用到的寄存器压入栈中;在函数返回前将栈中元素弹出,从而恢复寄存器原来的值(注意顺序!)
- 小技巧:如果批量传递连续的一组数据(比如字符串),此时不需要传递完整的字符串,只需要传递这组数据的首地址(即第一个元素的地址),以及这组数据的长度即可(
- 用(全局)变量传参
- 局限:当函数是一个递归函数时,函数多次自我调用会破坏变量中的参数值
- 用堆栈传参,有以下几种规范:
__cdecl(常用)- 参数按从右到左的顺序压入堆栈
- 参数的清理由调用者(caller)负责
- 当函数值是整数时由eax返回,是小数时则由st(0)返回
- eax、ecx、edx由调用者负责保存和恢复
- ebx、ebp、esi、edi由被调用者(callee)负责保存和恢复
- 这是C语言的参数传递规范
asmf: push bp mov bp, sp ; ... pop bp ret main: ... push a1 ; 压入参数 push a0 call f back: add sp, 4 ; 清理堆栈__pascal- 参数按从左到右的顺序压入堆栈
- 参数的清理由被调用者负责
- 这是Pascal语言的参数传递规范
asmf: push bp mov bp, sp ; ... pop bp ret 4 ; 假设a0、a1都是字数据 main: ... push a0 ; 压入参数 push a1 call f back:__stdcall- 参数按从右到左的顺序压入堆栈
- 参数的清理由被调用者负责
- 这是Windows API函数的参数传递规范
asmf: push bp mov bp, sp ; ... pop bp ret 4 ; 假设a0、a1都是字数据 main: ... push a1 ; 压入参数 push a0 call f back:
返回值的传递方式与传参类似(也可以把返回值放在(曾经)作为参数的寄存器或变量内),故不再赘述。
动态变量和堆栈框架
在函数的开头,需要用
push bp及mov bp, sp这两条指令来保护bp,同时构造堆栈框架(stack frame)构造好堆栈框架后,接着执行指令
sub sp, idata就可以在函数内部定义宽度为idata的动态变量或数组。这些动态变量是作用域在函数内的局部变量,因此在函数结束后会被丢掉(因此不能拿这些变量作为函数的返回值)。函数相关的堆栈框架如下所示(采用
_cdecl规范):  sp与bp之间的空间用于存放局部变量,而bp下面的空间存放的是参数。它们都可以借助bp来表示,其中局部变量可通过[bp - ...]被访问;而参数可通过[bp + ...]被访问,且[bp + 4]表示第一个参数,[bp + 6]是第二个参数,以此类推
在函数退出时先
mov sp, bp,此时sp回落bp的位置,局部变量全部失效,然后pop bp取出原来的bp值,再ret,此时pop出返回地址返回,然后在调用者处情况堆栈中的参数在函数中,除了要保护
bp外,还要保护bx、si、di这些偏移地址寄存器的值(在函数使用这些寄存器之前,将它们压入栈中;在函数返回前再弹出以恢复原值)综上,我们总结出一般的函数写法:
f:
push bp
mov bp, sp
sub sp, ...
push bx
push si
push di
... ; [bp+?] 为参数
... ; [bp-?] 为局部变量
mov ax, ... ; 设置返回值
pop di
pop si
pop bx
mov sp, bp
pop bp
ret递归函数
??? example "例子:求累加和"
```asm
code segment
assume cs:code
;Input: n=[bp+4]
;Output: AX=1+2+3+...+n
f proc near
push bp ; (3)(6)(9)
mov bp, sp
mov ax, [bp+4]
cmp ax, 1
je done
dec ax
push ax ; (4)(7)
call f ; (5)(8)
there:
add sp, 2 ; (12)(15)
add ax, [bp+4]
done:
pop bp ; (10)(13)(16)
ret ; (11)(14)(17)
f endp
main:
mov ax, 3
push ax ; (1)
call f ; (2)
here: ; f(3)的返回值在AX中, 值为6
add sp, 2 ; (18)
mov ah, 4Ch
int 21h
code ends
end main
```
中断
中断:通俗理解为,CPU不再继续往下执行,而是转去处理来自CPU内部或外部设备的特殊信息。
分类:
- 根据中断的来源:
- 软件中断:在代码显示地用
int n指令来调用中断例程(属于内中断,来自程序员) - 硬件中断:由硬件的某个事件触发,并由CPU自动插入一个隐式的
int n指令来调用中断例程(来自硬件)
- 软件中断:在代码显示地用
- 根据中断的硬件来源:内中断、外中断
- 根据中断的来源:
中断信息包含用于识别来源的编码,称为中断类型码,它是一个字节型数据,可以表示256种中断信息的来源(尽管实际上并没有256种中断)
中断向量表:存放一系列中断向量(即中断处理程序的入口地址)的列表。
- 在8086PC机中,它被存在内存0000:0000~0000:03FF的1024个字节中
- 每个中断向量占2个字(4字节),高地址字存放段地址,低地址字存放偏移地址
- 一般情况下,中断向量表中0000:0200~0000:02FF的256字节空间是空的,操作系统和其他应用程序不会占用,因此可利用这块空间来自定义中断
中断处理程序/中断例程:用于处理中断信息的程序,被中断向量定位。执行步骤为:
保存用到的寄存器
处理中断
恢复用到的寄存器
用
iret指令返回iret指令的等价操作:
cip = word ptr ss:[sp]; cs = word ptr ss:[sp+2]; fl = word ptr ss:[sp+4]; sp += 6;
中断过程:
- 取得中断类型码N
pushf- tf = 0, if = 0
push cspush ip- ip = N * 4, cs = N * 4 + 2
自定义中断,大致过程为:
编写中断处理程序(与编写一般函数类似)
安装中断处理程序:将中断处理程序存在不太可能被覆写的内存中(一般存在0000:0200~0000:02FF这个空的中断向量表空间内),一般借助
rep movsb指令完成这一步骤- 例子:
n号中断处理程序的标号为int_handler,其入口地址设为seg_addr:ofs:addr,那么安装过程为:
asmassume cs:code code segment main: ; 设置ds:si指向源地址(中断处理程序) mov ax, cs mov ds, ax mov si, offset int_handler ; 设置es:di指向目标地址(某个安全的内存块) mov ax, seg_addr mov es, ax mov bi, ofs_addr ; cx的值设为中断处理程序的长度,通过两个标号的地址之差计算得到 mov cx, offset int_handler_end - offset int_handler cld ; 令 df = 0 rep movsb ; copy! ;设置中断向量表 mov ax, 4C00h int 21h ; 中断处理程序 int_handler: ; ... ; ... int_handler_end: nop code ends end main- 例子:
修改中断向量表:将中断处理程序的入口地址存在中断向量表的对应表项中
- 例子:修改后的
n号中断处理程序的入口地址为seg_addr:ofs:addr,中断向量表的修改过程如下所示:
asmmov ax, 0 mov es, ax mov word ptr es:[n*4], ofs_addr mov word ptr es:[n*4+2], seg_addr- 例子:修改后的
有些情况下,CPU不会响应中断
- 比如执行向ss寄存器传送数据的指令时,中断不会发生,这是为了避免对ss:sp整体的破坏
内中断
内中断的几种情况:
除法错误
- 中断类型码:0
单步执行
- 中断类型码:1
- 中断发生条件:当陷阱标志位tf = 1时,CPU在每执行完一条指令后,会自动在该条指令与下条指令之间插入一条
int 1h指令并执行它 - 功能:在Debug中,t命令起到单步中断的功能,执行该命令后会显示各个寄存器的状态并等待继续输入(中断处理程序的作用)
- 为了不让程序一直陷入单步中断的循环中,所以中断过程中要将tf设为0
执行
into指令(溢出中断)- 中断类型码:4
- 等价操作:
cif (of == 1) { old_fl = fl; if = 0; tf = 0; sp -= 6; word ptr ss:[sp] = ip + 1; word ptr ss:[sp+2] = cs; word ptr ss:[sp+4] = old_fl; ip = word ptr 0000:[0010h] cs = word ptr 0000:[0012h] }执行
int n指令- 中断类型码:n(自己指定的中断码,一个字节型立即数)
- 等价操作:
cold_fl = fl; if = 0; tf = 0; sp -= 6; word ptr ss:[sp] = ip + 2; word ptr ss:[sp+2] = cs; word ptr ss:[sp+4] = old_fl; ip = word ptr 0000:[idata8 * 4] cs = word ptr 0000:[idata8 * 4 + 2]- 格式:
asmint idata8- 该指令的机器码为2字节:
0CDh, idata8,其中idata8是中断号 - 该指令的目标地址是一个32位的远指针,称为中断向量(interrupt vector),被保存在0000:idata8*4处
- [0000:0000, 0000:03FFh]这个内存区间称为中断向量表,一共存放了从
int 00h到int 0FFh共256个中断向量 - BIOS和DOS为程序员提供了诸多中断功能,下面将会列出几种常见功能(其他功能参见中断大全)
??? info "BIOS和DOS中断例程的安装过程"
1. 开机时CPU通电后,初始化cs = 0FFFFH,ip = 0,因此执行0FFFFH:0处上的跳转指令,执行该指令后转去执行硬件系统检测和初始化程序
2. 初始化程序将建立BIOS所支持的中断向量表(注意中断例程固化在ROM中,是一直在内存中存在的)
3. 完成上述步骤后,调用`int 19h`进行操作系统的引导,控制权交给操作系统(DOS)
4. DOS系统会将中断例程装入内存,并建立相应的中断向量表
DOS 中断
int 03h- 功能:软件断点中断
- 等价操作:
cold_fl = fl; if = 0; tf = 0; sp -= 6; word ptr ss:[sp] = ip + 1; word ptr ss:[sp+2] = cs; word ptr ss:[sp+4] = old_fl; ip = word ptr 0000:[000Ch] cs = word ptr 0000:[000Eh]int 21h输入输出相关:
ah = 01h号功能:输入字符al保存读入的字符
ah = 02h号功能:输出字符dl保存待输出的字符,如果是数字则看作ASCII码
ah = 09h号功能:输出字符串- ds:dx指向一个以
$为结尾的字符串的首地址,显示的字符串不包含这个$
- ds:dx指向一个以
ah = 0Ah号功能:输入字符串- ds:dx指向一个buf,buf的第一个字节为允许输入的最多字符数,第二个字节为实际输入的字符数,从第三个字节开始才是输入的字符内容
- 如果输入超过最大字符数,则会发出铃声,并且光标不再移动
文件操作相关:
ah = 3Ch号功能:创建文件cx =文件属性(0:可写,1:只读),ds:dx指向文件名的首地址- 返回值:
- 成功:
ax =句柄,cf = 0 - 失败:
ax =错误码,cf = 1
- 成功:
ah = 3Dh号功能:打开文件al =打开方式(0:只读,1:只写,2:可读可写),ds:dx指向文件名的首地址- 返回值:
- 成功:
ax =句柄,cf = 0 - 失败:
ax =错误码,cf = 1
- 成功:
ah = 3Eh号功能:关闭文件bx =句柄- 返回值:
- 成功:cf = 0
- 失败:
ax =错误码,cf = 1
ah = 3Fh号功能:读文件bx =句柄,cx =待读字节数,ds:dx指向一块buf,用于存储读入的数据- 返回值:
- 成功:
ax =已读字节数,cf = 0 - 失败:
ax =错误码,cf = 1
- 成功:
ah = 40h号功能:写文件bx =句柄,cx =待写字节数,ds:dx指向一块buf,用于存储写出的数据- 返回值:
- 成功:
ax =已写字节数,cf = 0 - 失败:
ax =错误码,cf = 1
- 成功:
ah = 42h号功能:移动文件指针bx =句柄,al =移动的参照点(0:文件首字节的位置,1:文件指针当前位置,2:EOF,即文件末字节位置 + 1),cx:dx = 移动的距离(可正可负,正数表示指针向右移)- 返回值:
- 成功:ds:ax = 当前文件指针与文件首字节的距离,cf = 0
- 失败:
ax =错误码,cf = 1
内存分配相关:
ah = 48h号功能:分配内存bx =待分配内存块的节长度- 返回值:
- 成功:
ax =段地址,cf = 0 - 失败:
ax =错误码,cf = 1,bx =最大内存块的节长度
- 成功:
ah = 49h号功能:释放内存es =待释放内存块的段地址- 返回值:
- 成功:cf = 0
- 失败:
ax =错误码,cf = 1
ah = 4Ah号功能:重分配内存bx =重分配内存块的节长度- 返回值:
- 成功:cf = 0
- 失败:
ax =错误码,cf = 1,bx =最大内存块的节长度
ah = 4Ch号功能:程序返回(控制权交给父程序,比如DOS)al用于表示返回值,一般设为0
外中断
外中断分为2类:
可屏蔽中断:CPU可以不响应的外中断
当中断标志位if = 1时,响应中断,否则不响应此类中断(可以用
sti或cli指令分别设置if的值为1或0)- 小技巧:用
cli和sti包围起来的指令不会被中断
asmcli ; ... ; instructions ; ... sti- 小技巧:用
除了中断类型码是通过CPU的数据总线传进来的之外,其余中断过程与内中断相同
大多数由外设引起的外中断属于此类中断
不可屏蔽中断:CPU必须相应的外中断
- 中断类型码固定为2
- 中断过程为:
- 标志寄存器入栈,if = 0, tf = 0
- cs、ip入栈
- ip = 8, cs = 0AH
BIOS 中断
??? info "BIOS里有什么?"
- 硬件系统的检测和初始化程序
- 外部中断和内部中断的中断例程
- 用于对硬件设备进行I/O操作的中断例程
- 其他和硬件系统相关的中断例程
int 09h:处理键盘输入- 通过
60h号端口读取键盘输入的扫描码,并转化为相应的ASCII码以及状态信息,存在内存的指定空间中(键盘缓冲区或状态字节)中
- 通过
int 10hah = 00h号功能:切换显示模式al = 03h表示 80*25 文本模式al = 13h表示 320200256 图形模式
ah = 02h号功能:设置光标位置bh用于设置光标所在页数,1页即为文本模式的显示缓冲区,共8页dh用于设置光标所在行号dl用于设置光标所在列号
ah = 09h号功能:在光标位置显示字符al用于表示字符bl用于设置字符颜色,每个位都有不同的含义,具体可见硬件基础知识中的“显卡地址映射”部分的文本模式cx用于表示字符重复个数
int 16h:从键盘缓冲区读取一个键盘输入,并将其从缓冲区中删除需要先设置
ah = 0ah保存扫描码,al保存ASCII码(其实也可以读取方向键、功能键、PgUp等键,但不能读取单独的Ctrl键)配合
int 09h实现键盘读取
混合语言编程
不想学了(应该不会考吧)...
保护模式
不想学了(应该不会考吧)...
对此感兴趣的读者可以去看TonyCrane老师的笔记~