x86汇编语言复习笔记

时间:2021-08-18 14:10:14

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 图解移位指令

x86汇编语言复习笔记

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的长度。

  则可以画出该数据段内存示意图如下:

  x86汇编语言复习笔记

 

  其中一个英文字母占据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)内存图如下:

  x86汇编语言复习笔记

  说明:对于字和双字的定义,低字节在低位,因此如对于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汇编语言》这门课程是本学期选的最成功的一门课程,熊老师对学生也十分认真负责。这也再次印证了那个真理,那就是只有实践,才能真正把理论中的内容理解、消化。

  从最开始的连课都听不懂、程序写不出、编程毫无头绪,到后来经历了几次作业的历练后,思路渐渐清晰,我不得不十分感谢熊老师的严格要求。

  汇编是一种十分贴近计算机底层的语言,它深刻的揭示了程序运行的过程以及内存的使用和分配机制,在本科阶段,有汇编编程的锻炼经历,我认为是十分有必要的。

  最后,在编写这篇笔记的过程中,还要特别感谢小马哥给我提出的宝贵的修改意见!

  明天就要期末考试了。真心的希望先先能够发挥高水平,取得好成绩。与各位共勉!