0 写在前面
为了更深入的了解程序的实现原理,近期我学习了IBM-PC相关原理,并手工编写了一些x86汇编程序。
在2017年的计算机组成原理中,曾对MIPS体系结构及其汇编语言有过一定的了解,考虑到x86体系结构在目前的广泛应用,我通过两个月左右的时间对x86的相关内容进行了学习。
在《x86汇编语言实践》系列中(包括本篇、x86汇编语言实践(1)、x86汇编语言实践(2)、x86汇编语言实践(3)以及x86汇编语言实践(4)),我通过几个具体案例对x86汇编语言进行实践操作,并记录了自己再编写汇编代码中遇到的困难和心得体会,与各位学习x86汇编的朋友共同分享。
我将我编写的一些汇编代码放到了github上,感兴趣的朋友可以点击屏幕左上角的小猫咪进入我的github,或请点击这里下载源代码。
这是《x86汇编语言实践》系列的最后一篇文章,明天就要迎来x86汇编的期末考试了,希望所有朋友们以及先先能够考试顺利!
1 基础知识
1.Intel 8086/8088PC机的CPU字长为16位,16位的信息称为1个字,内存的基本单元为1个字节,但任何相邻两个单元都可以组成1个字。Intel 8086/8088PC机共有20根地址线,其寻址范围为00000H~FFFFFH。
2.用于间接寻址的寄存器有BX,SP,BP,SI,DI,其中,BX一般用于存放基址;在采用基址变址寻址时,采用SI或BX或DI寄存器,基址寻址默认的段是DS段(DS:[SI]);采用BP或SP寄存器,基址寻址默认的段是SS段(堆栈的位置和大小是由SP和SS共同决定的)。
3.串操作指令如MOVSB,STOSB,LODSB,SCASB,CMPSB,MOVSW,LODSW,STOSW,SCASW,CMPSW等,源操作数对应的地址是DS:[SI],目的操作数对应的地址是ES:[DI]。
4.Intel8086/8088CPU共有9个1为的标志寄存器(标志位),为了便于CPU的加工,他们被组合在一起形成一个16位的程序状态字寄存器PSW中。几个比较重要的标志位有:
- ZF:当运算结果为0时,ZF=1,否则ZF=0
- SF:运算结果为负时,SF=1,否则SF=0
- CF:算术运算最高位产生进位,CF=1.否则CF=0;还用于移位指令保存最高位左移或最低位右移移出的代码。
- DF:DF=1时每次串操作SI和DI减1,DF=0时每次串操作SI和DI加1。使用CLD可以将DF清零,即规定为正向操作字符串。
- TF:TF=1时执行完一条产生单步中断,中断处理程序将TF置0。TF标志用于调试。
- PF,AF,IF,OF这里我斗胆预测一啵,不考(因为真的没有使用过)。
5.STD是将DF置1的指令,与CLD将DF清零的效果相反。使用STD后,串指令对应的DI,SI寄存器每次操作后根据是SB还是SW操作自动减少1或2
6.逻辑地址向物理地址的转化(书上P24)。地址转化的动机:20位物理地址无法直接在16位字长的机器中直接运算,因此可以采用Intel的分段方法将其划分为16位段地址和16位段内地址(也称为偏移地址)逻辑地址的基本形式为0000H:0000H,该逻辑地址表示物理地址的00000H。
那么逻辑地址向物理地址的转换方式可以表述为以下公式:段地址x10H + 偏移地址 = 物理地址
举例说明:逻辑地址1234H:5678H转换为物理地址为:1234Hx10H + 5678H = 12340H + 5678H = 179B8H。再如:1234H:2001H = 12340H + 2001H = 14341H
7.几个重要的数据传送指令PUSH,POP,PUSHF,POPF
- PUSH SRC:先SP = SP - 2 再 SS:[SP] <- SRC
- PUSHF :先SP = SP - 2 再 SS:[SP] <- PSW
- POP SRC:先SRC <- SS:[SP] 再 SP = SP + 2
- POPF :先PSW <- SS:[SP] 再SP = SP + 2
总结而言,PUSH和POP是将操作数压(弹)栈,POPF和PUSHF是将PSW标志寄存器压(弹)栈
2 寻址方式
2-1 六种与数据有关的寻址方式
2-1-1 立即寻址
直接将立即数写到指令中的寻址方式。注意不得超出寄存器的字节范围:AL8位,AX16位。
【例】
- AND AX,0FFFEH
- MOV AL,100H
- MOV AL,00000101B
- MOV AX,512
【不能使用】
- MOV AL,100H
- MOV AX,10000H (超出了字节范围)
2-1-2 寄存器寻址
使用寄存器的寻址方式。可以显示使用,也可以隐式使用。也可以使用段寄存器CS,DS,SS,ES。
【例】
- MOV DS,AX
- PUSH DS
- PUSHF (隐式操作PSW)
- STD (隐式操作PSW)
- CMC (对CF取反操作,隐式操作PSW)
2-1-3 直接寻址
直接使用操作数的偏移地址进行寻址的方式,偏移地址用[立即数]的形式表示,或者直接用数据段中定义的变量名表示,或用数据段中定义的变量名+立即数的形式表示。
【例】
- AND AX,[0FFFEH]
- MOV AX,X ;其中X为数据段中定义好的数据
- MOV AX,STR+1 ;其中STR为数据段中定义好的数据,STR+1直接寻址到STR下一个字节单元的内容
2-1-4 寄存器间接寻址
使用寄存器中存储的偏移地址进行寻址。注意只能使用寻址寄存器BX,BP,SI,DI进行寻址,而不能用DX等进行寻址。此外,寻址的地址必须为16位,即不能使用BL等进行寻址。
【例】
- MOV AX.[BX]
- MOV BH,[BP]
- MOV CX,[SI]
- MOV DL,[DI]
以上四条指令等价于
- MOV AX.DS:[BX]
- MOV BH,SS:[BP]
- MOV CX,DS:[SI]
- MOV DL,ES:[DI]
但是在每条指令前加上一个段超越的段名,既麻烦又没必要,因此通常都默认缺省为上述隐含段规则。
【不能使用】
- MOV AX,[DX] (不能用DX)
- MOV DL,[BL] (必须为16位寻址)
2-1-5 寄存器相对寻址
在寄存器间接寻址的基础上,再增加一个常偏移量。形式多变,大致有如下几种
【例】
- MOV AX,[BX+100]
- MOV AX,[SI+10H] <==> MOV AX,10H[SI]
- MOV AX,ARRAY[SI]
- MOV TABLE[DI],AL
- MOV TABLE[DI+1],AL 3~5展示了立即数也可以是数据段中定义好的变量名
最终在debug下所有的寻址有效地址会被计算成[DI+XXXX]的形式,XXXX是一个十六进制数。
2-1-6 基址变址寻址
即基址加变址寻址方式,基址采用BX,BP寻址,变址采用DI,SI寻址,寻址规则相对固定。
【例】
- MOV AX,[BX][SI]
- MOV AX,[BX+SI]
- MOV ES:[BX+SI],AL
- MOV [BP+DI],AX
- MOV AX,[BX+SI+200]
- MOV ARRAY[BP+SI],AX
其中,段取决于基址寄存器,如BX的段就默认为DS;BP缺省为SS。当然,有指定段的情况除外。也可以在两个寄存器加和的基础上再增加一个立即数。
【不能使用】
- MOV [BX+CX],AX (CX不能做变址寄存器)
- MOV [BX+BP],AX (BP不能做变址寄存器)
- MOV [BX+DI],ARRAY (两个全在内存中的操作数,不符合语法)
2-1 五种与转移地址有关的寻址方式
2-2-1 标号与过程名
与转移地址相关的指令主要是JMP和CALL指令,而要让代码能够跳跃执行到指定的IP处则需要通过标号指示某行代码,或是通过过程名定义进行CALL调用。
2-2-2 段内直接寻址
即直接使用标号与过程名进行跳转。根据位移量的不同,可以加SHORT(8BITS)和NEAR PTR(16BITS)操作符。其中,条件跳转只能是8位因此省略SHORT,而JMP则缺省为16位位移量。因此,在跳转位移已知的前提下,使用JMP SHORT可以提高程序的执行效率。
【例】
- JMP L1
- CALL P1
- JMP SHORT L1
- JMP NEAR PTR L1 (L!与当前IP位移量为16位的数值)
2-2-3 段内间接寻址
即将转移目的地址放入寄存器中进行存储,调用的也是寄存器中的相应数值。
【例】
- MOV AX,OFFSET P1 CALL AX
- JMP BX
这里要特别注意段内间接寻址与数据寻址中寄存器间接寻址的区别,后者有[]进行寻址。
- MOV AX,OFFSET P1 MOV ADD1,AX CALL ADD1
- MOV BX,OFFSET ADD1 CALL [BX]
以上两种也是段内间接寻址,注意这里ADD1不是过程名,而是数据段中的一个数据的地址,存放了子程序P1的位移量。BX则存放了ADD1的地址,因此调用CALL [BX]也属于段内间接寻址。
2-2-4 段间直接寻址
具备FAR属性的寻址。例如P2为一个有FAR属性定义的过程:
- CALL FAR P2
2-2-5 段间间接寻址
形式如下:
- JMP DWORD PTR [BX+INTERS]
只要DWORD PTR后面是除了立即寻址和寄存器寻址之外的任何一种数据寻址方式即可。
3 语法知识
【判断指令正误】
- MOV [CX],AL 不正确。CX不能作为寄存器间接寻址的寄存器
- MOV BH,320 不正确。320超出了8位范围(255)
- MOV DS,2000H 不正确。不存在从立即数到段寄存器的数据通路。此外,段寄存器作目的操作数时,不允许使用CS作为目的操作数。
- ADD SI,FDDH 不能确定。如果在数据段定义过一个名为FDDH的数据变量,且该数据在字节范围内,则此指令正确。否则会认为FDDH是一个未定义的变量,改成0FFDH后正确。
- SHL AX,2 不正确。移位指令格式中,移位的数量count只能是1或CL。移动位数大于1(0和1也可以)必须放入CL寄存器中操作。
- CMP BYTE PTR [SI],X 不正确。源操作数和目的操作数不能同时为内存中的数。
- LEA BX,[SI] 正确。
- LDS BX,[DX] 不正确。DX不能用作寄存器间接寻址的寄存器
- JMP BYTE PTR AX 不正确。转移只有NEAR/FAR PTR + 标号或SHORT+标号或只有标号/寄存器的形式。没有JMP BYTE PTR的形式
- JMP AX 正确。
- JMP [AX] 不正确。AX不能用作间接寻址的寄存器。
- RET 5 不正确。后面的立即数必须为偶数。这是为了带参数调用的子程序在返回时要弹出几个参数的位置,进而维持堆栈的平衡。
- MOV [BX+SI+10],100 不正确。注意只有在寄存器相对寻址取数(作为源操作数)时直接寻址即可,若作为目的操作数则必须指定size。修改为MOV BYTE PTR[BX+SI+10],100即可。
- DIV AL 正确。执行结果为AX=0001即除以本身,商1余0。
4 简答问题
4-1 解读指令执行过程
1. RET EXP
IP ← [SP]
SP ← SP + 2
SP ← SP + EXP
2.RETF
注意如果是FAR属性的过程,返回时是段间返回(即在汇编器中会被汇编成RETF指令),会执行以下过程
IP ← [SP]
SP ← SP + 2
CS ← [SP]
SP ← SP + 2
3.PUSH SRC
SP ← SP - 2
SS:[SP] ← SRC
4.PUSHF
SP ← SP - 2
SS:[SP] ← PSW
5.POP DST
DST ← SS:[SP]
SP ← SP + 2
6.POPF
PSW ← SS:[SP]
SP ← SP + 2
7.LEA REG,SRC
将SRC的偏移地址送入REG
8.LDS/LES REG,SRC
将SRC中的双字内容分别送REG和DS/ES中。
这里的SRC中的双字通常保存的是某个程序或变量的逻辑地址(SEG:OFFSET),前面的低字送入REG,后面的高字送入DS/ES。
9.CALL FAR PTR P1
SP ← SP - 2
SS:[SP] ← 返回地址段值
SP ← SP - 2
SS:[SP] ← 返回地址偏移值
IP ← 目的偏移地址
CS ← 目的段地址
10.CALL AX(假设为段内间接调用)
SP ← SP - 2
SS:[SP] ← 返回地址偏移值
IP ← AX中有效地址
11.JMP [BX]
从内存中根据BX间接寻址,取得的标号值送IP进行跳转。
12.JMP DX
将DX中有效地址偏移值送入IP进行跳转。
13.CALL DWORD PTR [BX]
段间间接调用,过程地址CS:IP(这是一个双字,因此用DWORD PTR)位于数据段中通过BX间接寻址得到。
14.LOOP LP1
CX = CX - 1
若CX ≠ 0,则跳转至LP1,否则顺序执行之后代码。
15.CMP BX,X1
分别通过寄存器寻址和直接寻址取得BX与X1的值,计算BX-X1并影响标志位。(如可用ZF判断两数是否相等、CF=1或SF=1则BX<X1等等,再配合JC,JS等指令即可进行条件跳转)
16.INT 21H / IRET
Intel8086/8088指令系统中用于支持中断调用的指令为INT n,返回中断的指令时IRET。此外,CLI用于清除中断标志,STI用于设置中断标志。
----《书》P173
INT 21H的执行过程:
- SP ← SP - 2
- SS:[SP] ← PSW
- SP ← SP - 2
- SS:[SP] ← INT N 下一条指令的CS
- SP ← SP - 2
- SS:[SP] ← INT N 下一条指令的IP
- IP ← [0000 : N*4]
- CS ← [0000 : N*4+2]
IRET的执行过程:
- IP ← SS:[SP]
- SP ← SP + 2
- CS ← SS:[SP]
- SP ← SP + 2
- PSW ← SS:[SP]
- SP ← SP + 2
4-2 图解移位指令
4-3 指出目的寄存器中的内容
已知:DS = 2100H,BX = 0100H,SI = 0002H;内存中:[21100H] = 12H,[21101H] = 34H,[21102H] = 56H,[21103H] = 78H。
- MOV AX,[101H] ;直接寻址,默认段DS。AX的结果为3456H ?在DOS下,汇编后变成了MOV AX,101这种立即寻址形式,使得AX最终的结果为0101H
- MOV AX,WORD PTR [BX+2] ;寄存器相对寻址。AX结果为7856H
- MOV AL,BYTE PTR [BX][SI+1] ;基址变址寻址。AL结果为78H
- MOV AX,100H [SI] ;寄存器相对寻址。AX结果为7856H(注意取出的为一个字!而且是小端存储!低字节在高位!)
4-4 指出CS与IP的值
已知:DS = 2100H,BX = 0101H,CS = 1900H;内存中:[21101H] = 0C7H,[21102H] = 0FFH,[21103H] = 00H,[21104H] = 0F0H。
- JMP BX ;CS = 1900H,IP=0101H
- JMP [BX] ;CS = 1900H,IP=0FFC7H
- JMP WORD PTR [BX+1] ;CS = 1900H,IP=00FFH (注意小端存储,低字节在低地址)
- JMP DWORD PTR [BX] ;CS = 0F000H,IP=0FFC7H (取双字分别取得的是段地址与偏移值)
4-5 根据要求画内存示意图
1.定义MYSEG数据段,其中有S1,内容'ABCD'以00H结尾;S2是能用AH=9,INT 21H显示的字符串;S3为10x10的二维字数组。L1为S1+S2+S3的长度。
则可以画出该数据段内存示意图如下:
其中一个英文字母占据1字节,即一个内存单元;显示字符串必须以'$'结尾。常量定义形式应为 L1 EQU $-S1。注意,字数组一个字占两个字节。故L1的值为210。
2.书P96第2题的数据段可以定义如下:
1 DATA SEGMENT PARA 2 X1 DB 'Display string',0DH,0AH,'$' 3 X2 DB 32 4 X3 DW 40H 5 X4 DD A000H,0120H 6 X5 DW 10 DUP(8 DUP(0)) 7 X6 EQU $-X1 8 DATA ENDS
4-6 综合练习题
【题签】有数据段定义如下:
1 DATA1 SEGMENT PARA 2 X1 DB 20H,?,'A' 3 X2 DW 2 DUP(1,2DUP(1,?)) 4 X3 DD 12345678H 5 LEN EQU $-X2 6 DATA1 ENDS
(1)画出内存图
(2)执行MOV AX,X3+1后,AX为?
(3)执行MOV CX,LEN后,CX为?
【解】
(1)内存图如下:
说明:对于字和双字的定义,低字节在低位,因此如对于DW 1234H来说,在内存中由低地址到高地址依次为34H、12H;对于DD 12345678H而言,在内存中由低地址到高地址依次为78H、56H、34H、12H。而对于数组的定义而言,如DUP,则是按照其定义先后顺序在内存中由低到高排列的。必须注意的是:由于前面定义的是字DW,所以DUP中的每一个数值都占据两个内存单元,即1个字的空间,这在画内存图时必须要注意!
(2)AX = 3456H这道题这里有点小bug,编译后会报告1个warning,更好的改进是使用MOV AX,WORD PTR X3+1。
这道题可以改进成一个更有意思的考法:MOV AX,WORD PTR X3+2
这样以来就要联系(1)中画的内存图了。内存中高地址存放的是数据中的高字节。因此结果应该是AX=1234H
(3)CX = 18H(可以表示为16进制,一定注意数组定义DW DUP的问题!这会对LEN的计算产生影响)
5 编程题
5-1 加法
计算Z=X+Y。其中X,Y为16位数,Z为32位数。
1 XOR DX,DX 2 MOV AX,X 3 ADD AX,Y 4 ADC DX,0 5 MOV WORD PTR Z+2,DX 6 MOV WORD PTR Z,AX
这里引入一个技巧:为了操作32位数,我们需要借用DX:AX进行操作,我们一个一个地计算这两个寄存器中的数值,低位产生的进位补到DX中去,使用ADC指令。最后为内存中的Z使用WORD PTR进行赋值即可。
5-2 右移
将32位X右移4位。
1 MOV AX,WORD PTR X 2 MOV DX,WORD PTR X+2 3 SHR DX,1 4 RCR AX,1 5 SHR DX,1 6 RCR AX,1 7 SHR DX,1 8 RCR AX,1 9 SHR DX,1 10 RCR AX,1
必须要注意的是这里必须使用SHR与RCR指令配合4次,每次移动1位进行使用,这是由于,CF只能存放1位数字!
5-3 乘法
用移位及加法指令,将32位数X计算X = X * 10。
1 MOV AX,WORD PTR X 2 MOV DX,WORD PTR X+2 3 SHL AX,1 4 RCL DX,1 5 MOV BX,AX 6 MOV CX,DX 7 SHL AX,1 8 RCL DX,1 9 SHL AX,1 10 RCL DX,1 11 ADD AX,BX 12 ADC DX,CX 13 MOV WORD PTR X,AX 14 MOV WORD PTR X+2,DX
这里用到的技巧是将X*10分解成X*2 + X*8来计算,也就是将X左移1位保存下来再左移2位加上刚才保存的值即可。
5-4 打印
将内存中16位X显示为十六进制ASCII码。
1 MOV BX,X 2 MOV CX,4 3 LP: 4 PUSH CX 5 MOV CL,4 6 ROL BX,CL 7 MOV AL,BL 8 AND AL,0FH 9 ADD AL,30H 10 CMP AL,39H 11 JBE DISP 12 ADD AL,7 13 14 DISP: 15 MOV DL,AL 16 MOV AH,2 17 INT 21H 18 POP CX 19 LOOP LP
注意以下几点技巧:
- 16位数字X需要输出4位数字,因此设置CX的值为4作为外层循环次数
- 输出每次对X的值进行循环左移4位(不带CF)的ROL指令,这样每次BL的低4位即为当前要输出的值
- 由于又用到了CL,因此外层循环的CX需要在第4行处压栈处理
- 由于每次输出只有4位,而最少取出AL为8位,因此需要使用第8行AND AL,0FH来屏蔽AL的高4位
- 需要注意的是,在16进制中超过9的数字变成了A,由于ASCII码中‘9’与‘A’之间相差8,因此需要判断是否需要给AL增加相应值,使用的是ADD AL,7实现
- 输出单个字符的中断调用为2号中断调用
6 写在最后
熊老师的《x86汇编语言》这门课程是本学期选的最成功的一门课程,熊老师对学生也十分认真负责。这也再次印证了那个真理,那就是只有实践,才能真正把理论中的内容理解、消化。
从最开始的连课都听不懂、程序写不出、编程毫无头绪,到后来经历了几次作业的历练后,思路渐渐清晰,我不得不十分感谢熊老师的严格要求。
汇编是一种十分贴近计算机底层的语言,它深刻的揭示了程序运行的过程以及内存的使用和分配机制,在本科阶段,有汇编编程的锻炼经历,我认为是十分有必要的。
最后,在编写这篇笔记的过程中,还要特别感谢小马哥和乔给我提出的宝贵的修改意见!
明天就要期末考试了。真心的希望先先能够发挥高水平,取得好成绩。与各位共勉!