第十章 CALL 和 RET 指令
● call 与 ret 指令均为转移指令,用于子程序实现
● ret 用栈中数据修改 IP 实现段内转移,执行本质:pop IP,或表示成 IP = ((ss) * 16 + (sp)),sp = (sp) + 2(括号表求值),以下代码展示一个从腰开始、到头结束的程序
1 assume cs:codesg 2 3 stack segment 4 db 16 dup (0) 5 stack ends 6 7 codesg segment 8 mov ax, 4c00h 9 int 21h 10 start: 11 mov ax, stack ; 手动维护一个栈 12 mov ss, ax 13 mov sp, 16 14 mov ax, 0 ; 把 0 入栈 15 push ax 16 mov bx, 0 ; 做一些其他事 17 ret ; IP = 0,CS:IP 指向第一条指令 18 codesg ends 19 20 end start
● retf 用栈中数据修改 CS 与 IP 实现段间转移,执行本质:pop IP pop CS,以下代码类似
1 assume cs:codesg 2 3 stack segment 4 db 16 dup (0) 5 stack ends 6 7 codesg segment 8 mov ax, 4c00h 9 int 21h 10 start: 11 mov ax, stack 12 mov ss, ax 13 mov sp, 16 14 mov ax, 0 15 push cs ; 多一个 cs 入栈 16 push ax 17 mov bx, 0 18 retf 19 codesg ends 20 21 end start
● call mark 实现 IP 压栈后段内跳转,执行本质:push IP jmp near PTR mark
● call far mark 实现 CS、IP 压栈后段间跳转,执行本质:push CS push IP jmp far PTR mark
● call reg16,call word PTR addr ,call dword PTR addr 类似实现压栈和跳转
● 非溢出的除法,XH 与 XL 表 X 的高 16 位和低 16 位:X / N = XH / N * 65536 + ((X - XH / N * N) * 65536 + XL) / N
第十一章 标志寄存器(这部分的例子写的都挺好)
● 8086 CPU标志寄存器(16 位)中存储的信息称为程序状态字(PSW)。从高位(15)到低位(10)依次为:无,无,无,无,OF(11),DF,IF,TF,SF,ZF(6),无,AF(4),无,PF(2),无,CF(0)
● ZF 零标志位,PF 奇偶标志位,SF 符号标志位,CF 进位标志位,OF 溢出标志位,DF 方向标志位,
● SF 理解:将数据当有符号数运算时,可通过 SF 得知结果正负,将数据当无符号数运算时,SF 无意义,虽然指令可能影响了其值
● CF 理解:无符号运算最高有效位(依寄存器,不依实际数值)向假想的更高位的进位和借位的情况
● OF 理解:有符号运算是否溢出(正 / 负)的情况
● 好例,说明 CF 和 OF 之间并没有关系:
mov al, 62h add al, 63h ; 执行后 CF = 0(无符号加法 98 + 99 = 197 < 255),OF = 1(有符号加法 98 + 99 = 197 > 127)
mov al, F0h add al, 88h ; 执行后 CF = 1(无符号加法 240 + 136 = 376 > 255),OF = 1(有符号加法 -16 - 120 = -136 < -128)
mov al, F0h add al, 78h ; 执行后 CF = 1(无符号加法 240 + 120 = 360 > 255),OF = 0(有符号加法 -16 + 120 = 104)
● 指令 cmp 理解:两个操作数作差,但不输出结果,仅根据结果调整标志寄存器的数值。
● 无符号数比较,执行 cmp ax, bx 后,根据 ZF 和 CF 来判断两个操作数的情况
ZF = 1,说明 ax == bx
ZF = 0,说明 ax != bx
CF = 1 或 ZF = 1,说明 ax ≤ bx
CF = 1,说明 ax < bx
CF = 0,说明 ax ≥ bx
CF = 0 且 ZF = 0,说明 ax > bx
● 有符号数比较(有溢出的存在,不能仅从 ZF 和 SF 判断,要考虑溢出标志位),执行 cmp ax, bx 后,
ZF = 1,说明 ax == bx
ZF = 0,说明 ax != bx
OF = 0 且 SF = 1,说明 ax < bx
OF = 1 且 SF = 1,说明 ax > bx。溢出后结果为负,说明是正溢出,正数减负数导致
OF = 0 且 SF = 0,说明 ax ≥ bx,配合 ZF判断是否取到等号
OF = 1 且 SF = 0,说明 ax < bx。溢出后结果为正,说明是负溢出,负数减正数导致
● 无符号条件转移及其等价伪代码
je 相等转移 if( ZF ==1 ) jmp
jne 不等转移 if( ZF ==0 ) jmp
jb 低于转移 if( CF == 1) jmp
jnb 不低于转移 if( CF == 0 ) jmp
ja 高于转移 if( CF == 0 && ZF == 0 ) jmp
jna 不高于转移 if( CF == 1 || ZF == 1 ) jmp
● DF 作用:每次串处理操作后 si,di 按 DF 值进行递增或递减
● pushf 与 popf 指令,将标志寄存器压栈或出栈。可用于访问和修改标志寄存器:
1 pushf 2 pop ax ; 标志寄存器的值出栈到 ax 中 3 4 ... ; 修改 ax 的值 5 6 push ax 7 popf ; ax 的值出栈到标志寄存器中
● 在 debug 中,标志寄存器直接按各个标志的值来显示,其对照关系如下:
寄存器 == 1 == 0
OF OV NV
DF DN UP
SF NG PL
ZF ZR NZ
PF PE PO
CF CY NC
第十二章 内中断
● 8086 CPU 内中断发生条件及中断类型码:除法错误(0),单步执行(1),into 执行(4),int 执行(n,字节型立即数)
● 中断向量表时中断处理程序入口地址的列表,在 8086 CPU 机器上占据内存首段 0000:0000 ~ 0000:03FF(1024 Byte),每个入口的段地址和偏移地址各占 1 Word(每入口 4 Byte),最多 256 入口。但是一般情况下 0000:0200 ~ 0000:02FF 都是空着的,操作协同和其他程序也不占用,debug 中查看如下
● 8086 CPU 中断过程:
① 从中断信息取得中断类型码 N
② 保存标志寄存器 pushf
③ 置标志寄存器 TF = 0,IF = 0
④ 保存当前指令位置 push CS,push IP
⑤ 计算中断程序入口 IP = (N * 4),CS = (N * 4 + 2)
⑥ 中断程序开始执行,保存要用到的寄存器,处理中断,回复用到的寄存器,用 iret 指令返回(iret 常与硬件自动完成的中断过程配合使用)
● 代码,自行改写 0 号中断,在发生除法溢出时执行自定义的程序(输出字符串 “overflow!”)
1 assume cs:code 2 3 code segment 4 start: 5 call div0 ; 向内存中注入 div0 代码(只需要运行一次,再次调用主程序时不用) 6 7 mov ax, 1000h 8 mov bh, 1h 9 div bh ; 计算 1000h / 1h,使得 al 中放不下结果,进入 0 号中断 10 11 mov ax, 4c00h 12 int 21h 13 14 div0: 15 mov ax, cs ; 设置 ds:si 指向中断程序源代码地址 16 mov ds, ax 17 mov si, offset do0 18 mov ax, 0 ; 设置 es:di 指向目标地址 0000:0200 19 mov es, ax 20 mov di, 200h 21 mov cx, offset do0end - offset do0 ; 设置 cx 为传输长度,用两个 offset 来计算 22 cld ; 设置传输方向为正 23 rep movsb ; 代码注入指定地址 24 25 mov ax, 0 ; 设置 es:0000 指向中断向量表 26 mov es, ax 27 mov word PTR es:[0*4], 200h ; 将第 0 个中断源指向注入的程序地址 28 mov word PTR es:[0*4+2], 0h 29 30 mov ax, 4c00h 31 int 21h 32 33 do0: 34 jmp short do0start ; 跳转到真正可执行的代码地址 35 db "overflow!" ; 将输出字符串放到 .code 域而不是 .data 域,否则代码注入时找不到 36 37 do0start: 38 mov ax, cs ; 设置 ds:si 指向字符串 39 mov ds, ax 40 mov si, 202h 41 mov ax, 0b800h ; 设置 es:di 指向显存空间的中间位置 42 mov es, ax 43 mov di, 12*160+36*2 44 mov cx, 9 ; 设置 cx 为字符串长度 45 46 s: ; 显示字符串 47 mov al, [si] 48 mov es:[di], al 49 inc si 50 add di, 2 51 loop s 52 53 mov ax, 4c00h 54 int 21h 55 56 do0end: ; 标记程序结束的地址,用于计算代码长度 57 nop 58 59 code ends 60 61 end start
■ 程序输出,注意 0000 : 0000 处入口地址发生了改变,0000 : 0200 处注入了程序,从右边可见 “overflow!” 的字符串
● 单步中断:CPU 检测到标志寄存器 TF = 1,则产生单步中断,其处理过程同一般中断过程,只是中断类型码为 1
● 有的情况下,即使 CPU 检测到中断信息,也不会发生响应。例如刚执行完 ss 寄存器的传送指令后,CPU 忽略中断,因为 ss : sp 指向栈顶,更改 ss 后可能指向敏感地址,不能调用中断等其他程序。此时应该将调整 sp 的指令紧接着调整 ss 指令存放。
第十三章 int 指令
● int 指令引发内中断,处理过程同一般中断过程
● iret 指令,放在需要返回原程序运行地址的中断例程的末端,等价于指令:pop IP,pop CS,popf,ret
● BIOS 主要包括几方面的内容:① 硬件系统检测室初始化程序;② 外部中断和内部中断的中断例程;③ 对硬件设备进行 I/O 操作的中断例程;④ 其他硬件系统相关的中断历程
● BIOS 安装到内存的过程:
① 开机 CPU 加电,初始化 CS = 0FFFFh,IP = 0,从 CS : IP 开始执行。该处有一条跳转指令,跳转执行 BIOS 硬件系统检测和初始化程序
② 初始化程序将 BIOS 支持的中断例程入口地址登记到内存的中断向量表中,中断例程本身是固化到 ROM 中的程序,一直在内存中存在
③ 硬件系统检测和初始化完成后,调用 int 19h 进行操作系统引导
④ DOS 操作系统启动,将其支持的中断例程填入内存
● 中断例程可以包含多个子程序,BIOS 和 DOS 的中断例程都使用寄存器 ah 来传递内部子程序编号
● 内存地址空间中,B80000h ~ BFFFFh 共 32 kB 空间用于 80 * 25 彩色自负模式的显示缓冲区,默认情况下显示第0页 B0000h ~ B8F9Fh 的内容
● 代码,在屏幕上显示文字,以及之前一直使用的程序退出(在这之前有屏幕输出的返利代码都不能正确输出,从这里开始可以用了)
1 assume cs:code 2 data segment 3 db 'Hello World!','$' ; 需要输出的字符串用 $ 标记结尾 4 data ends 5 6 code segment 7 start: 8 mov bh, 0 ; 第 0 页 9 mov dh, 3 ; 行号 10 mov dl, 9 ; 列号 11 mov ah, 2 ; 调用 BIOS 10 号中断例程 2 号子程序指定光标位置 12 int 10h 13 14 mov al, 3 ; 字符 ASCII 15 mov bl, 11001010b ; 颜色属性,闪烁(BL),背景(RGB),高亮(I),前景(RGB) 16 mov bh, 0 ; 第 0 页 17 mov cx, 8 ; 字符重复数 18 mov ah, 9 ; 调用 BIOS 10 号中断例程 9 号子程序打印字符 19 int 10h 20 21 mov ah, 2 22 mov bh, 0 23 mov dh, 5 24 mov dl, 9 25 int 10h 26 27 mov ax, data ; ds : dx 指向 data 地址 28 mov ds, ax 29 mov dx, 0 30 mov ah, 9 ; 调用 BIOS 21 号中断例程 9 号子程序输出字符串 31 int 21h 32 33 mov ax,4c00h ; 调用 BIOS 21h 号中断例程 4Ch 号子程序结束程序,指定返回值 00h 34 int 21h 35 36 code ends 37 38 end start ; 有数据段的时必须指定入口,否则暴死,无数据段时可以没有入口
■ 输出结果