汇编篇

80x86

指令系统

指令格式

[标号:] 操作码[操作数,...] [;注释]

整数指令

浮点数指令

操作系统行指令

7类寻址

  1. 立即寻址
  2. 寄存器寻址
  3. 直接寻址
  4. 寄存器间接寻址
  5. 相对寄存器间接寻址
  6. 基址加变址寻址
  7. 相对基址加变址寻址
立即寻址

把立即数送到寄存器中

1
2
3
MOV BL, 56H         ; 56H -> BL
MOV AX, 2056H ; 2056H -> AX
MOV ECX, 12345678H ; 12345678 -> ECX

立即数只能作源操作数,不能作目的操作数。

寄存器寻址

操作数在CPU的寄存器中,而寄存器的名由指令指出。

1
2
INC CL;
MOV AX, BX; (BX) -> AX BX的内容给AX
直接寻址

操作数有效地址直接包含在指令中,它位于操作码之后,存放在代码段中。如果指令无前缀指明在哪一段,则默认操作数存放在数据段。

1
MOV AX, [2000H];  DS段2000H的内容送AL,2001H的内容送AH。

逻辑地址 = 段基址 + 偏移地址

[]里面是偏移地址

段基址默认DS段

指定段加冒号

1
MOV AX, ES: [2000H]; ES段2000H的内容送AL,2001H的内容送AH。
寄存器间接寻址

操作数的偏移地址存放在寄存器中,而操作数存放在存储器中。

1
2
3
4
MOV AX, [BX];       AX <- (DS*10H+(BX))  
MOV AX, [BP]; AX <- (SS*10H+(BP))
MOV AX, ES: [BX]; AX <- ()
MOV AX, DS: [BP];

白话:比如存储器中有个数a地址为0x12345678 寄存器BX中存放的就是0x12345678 执行结果是把存放在BX中的0x12345678传送给AX

SS:堆栈段的段寄存器

相对寄存器间接寻址

寄存器内容与位移量之和形成操作数的有效地址。即

EA = [寄存器]+位移量

1
2
MOV ECX, 1500H[EAX]; DS段(EAX)+1500H中的双字数据送到ECX。
MOV ECX, [EAX + 1500H];
基址加变址寻址

基址寄存器内容与变址寄存器内容之和形成操作数的偏移地址。即

EA = [基址寄存器] + [变址寄存器]

1
2
3
4
基址寄存器:任何一个32位通用寄存器
变址寄存器:除ESP之外的任一个32位通用寄存器
MOV AX, [EBX+ECX]; DS段(EBX)+(ECX)中的字数据送AX
MOV AX, [EBX][ECX];
相对基址加变址寻址

EA == 有效地址 == 偏移地址

基址寄存器内容与变址寄存器内容再加偏移量之和形成操作数的有效地址。

EA = [基址寄存器]+[变址寄存器]+偏移量

1
2
3
4
MOV AX, 1234H[BX+DI];
MOV AX, [BX+DI+1234H];
MOV AX, 1234H[BX][DI];
# 几种方式等效,DS段(BX)+(DI)+1234H 中的字数据送AX
与跳转有关的寻址方式

无条件转移:JMP dst; dst:转移目标

调用语句: CALL dst; dst:调用目标

数据传送指令

交换指令XCHG

用于交换两个操作数

1
XCHG OP1, OP2

注意可以是两个寄存器或者一个寄存器与一个存储器。但不能是两个存储器

I/O指令 IN 和 OUT

用于在I/O端口和AL、AX或EAX累加器之间交换数据。

1
2
IN OP1, OP2   
OUT OP1, OP2
装入有效地址指令LEA
1
LEA OP1, OP2

将有效地址(偏移地址)送通用寄存器。

装入全地址指令LDSLESLFSLGSLSS
1
2
LDS reg, mem
...

取mem指示的32位或48位全地址指针(即一个16位段选择符和一个16位或32位偏移地址)装入段寄存器和16位或32位reg中

比如段基址和偏移地址都是16位,段基址送段寄存器,偏移地址动reg中。

例如:

1
2
X DD 12345678H
LDS SI , X; DS =1234H, SI = 5678H

对于16位和32位保护方式,选择符送段寄存器,偏移地址送reg中

例如

1
2
3
4
5
X DD 12345678H; 32位偏移值

DW 0010H; 16位选择符

LDS ESI, X; ESI=12345678H,DS=0010H

压栈/弹栈指令PUSH/POP

PUSH OP1

POP OP1

SS 堆栈段的第一个地址

SP 栈顶

全部通用寄存器压栈和出栈指令

PUSHA/POPA: 16位通用寄存器压栈和出栈指令。

入栈顺序为:AX、CX、BX、DX、SP、BP、SI和DI。

PUSHAD/POPAD: 32位通用寄存器压栈和出栈指令

入栈顺序为:EAX、ECX、EBX、EDX、ESP、EBP、ESI和EDI。

其中:SP和ESP为操作前的栈顶指针。

查表转换指令XLAT
1
XLAT;  AL <- ((BX)+(AL)) BX的内容加上AL的内容 得出一个地址,那个地址指向的内容传送给AL

算术运算指令

与运算有关的标志

CF: 进位/借位标志。加、减运算最高位产生进位/借位时置1

AF: 辅助进位/借位标志。加、减运算时低半字节产生进位/借位是置1。

OF: 溢出标志。有符号二进制加、减运算结果超出范围时置1。(最高位,次高位只有一个有进位则溢出)

ZF: 零标志。结果为零时置1。

SF: 符号标志。结果为负时置1。(最高位为1)

PF: 奇偶标志。 结果”1“的个数为偶数时置1。

加法和减法指令ADD/SUB
1
2
ADD OP1, OP2;  OP1 <- OP1+OP2, 置标志位
SUB OP1, OP2; OP1 <- OP1-OP2,置标志位

例如:AL=64H,AH=0A8H,求执行下列指令的结果和标志位的状态。

(1)ADD AL. AH

1
2
3
4
5
6
7
8
9
  0110 0100  // 100D 										通过补码运算		0110 0100

+1010 1000 // -88D - 0101 1000

1 0000 1100 // 12D (高位1舍弃) 0000 1100

| OF | SF | ZF | AF | PF | CF |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 0 | 0 | 0 | 0 | 1 | 1 |

1010 1000 最高位1为负数 求补码(取反加1)取反 0101 0111 -> 加1 0101 1000 -> -88D

(2)SUB AL, AH;

1
2
3
4
5
6
7
8
9
 1  0110 0100  // 100D 高位借1            0110 0100

-1010 1000 // -88D + 0101 1000

1011 1100 // -68D (188D) 1011 1100 //-68D 最高位为1负数,补码0100 0100 68D

| OF | SF | ZF | AF | PF | CF |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 1 | 1 | 0 | 1 | 0 | 1 |
带进位加法/减法指令 ADC/SBB
1
2
3
ADC OP1, OP2; OP1 <- OP1 + OP2 + CF

SBB OP1, OP2; OP1 <- OP1 - OP2 - CF

比如实现128位数相加 可以拆分为64 64 利用进位标志

加1/减1指令 INC/DEC
1
2
INC OP1; OP1 <- OP1 + 1
DEC OP1; OP1 <- OP1 - 1
交换加法指令XADD
1
2
XADD OP1, OP2; OP1 <- OP1+OP2  
; OP2 <- OP1
变反指令NEG
1
NEG OP1; OP1 <- -OP1 置标志位

例子:

1
2
3
4
5
6
7
8
NEG AL;
若(AL) = 13H, 执行NEG指令后,
(AL) = EDH

13H => 0001 0011
补码 => 1110 1101 => EDH

所以AL的内容为EDH

1110 1101

ED

比较指令CMP

将OP1减去OP2,但结果不存在OP1中,只使结果影响标志位。

1
CMP OP1, OP2; OP1 - OP2, 置标志位
A - B CF ZF SF OF
A = B - 1 - -
无符号数(看CF) A < B 1 0 - -
A > B 0 0 - -
有符号数 A < B - 0 1 0
A > B - 0 0 0
无符号乘法指令MUL
1
MUL OP1

被乘数隐含在累加器中(AL,AX,EAX),OP1为乘数。字节运算时乘积返回到AX;字运算乘积返回到DX:AX; 双字运算时乘积返回到EDX:EAX。

影响标志位OF和CF:

若积的高字节、字或双字为0,则OF、CF清0.否则OF、CF置1

比如8位乘8位结果还是8位则CF、OF为0

有符号乘法指令IMUL
单操作数
1
IMUL OP1

被乘数隐含在累加器中(AL,AX,EAX)

字节运算结果返回到AX

字运算 DX:AX

双字运算 EDX:EAX

双操作数
1
IMUL OP1, OP2;  OP1 <- OP1 * OP2
三操作数
1
IMUL OP1, OP2, OP3; OP1 <- OP2 * OP3

若积的高字节、字或双字不是积的符号位扩展,则OF、CF置1,否则OF、CF请0。

除法指令DIV/IDIV
1
2
DIV OP1; 无符号除法
IDIV OP1; 有符号除法

被除数隐含在AX中,

OP1 (除数) 被除数 余数 余数
字节 AX AL AH
DX:AX AX DX
双字 EDX:EAX EAX EDX

注意被除数位数要是除数的二倍16/8 32/16 64/32

那么8位被除数怎么办

符号扩展指令
1
2
3
4
5
6
7
CBW; 将AL中8位带符号数扩展为16位存入AX。

CWD; 将AX中16位带符号数扩展为32位存入DX:AX

CWDE; 将AX中16位带符号数扩展为32位存入EAX。

CDQ; 将EAX中32位带符号数扩展为64位存入EDX:EAX。

Tips:无论地址的位数如何,所指向的一个存储单元大小为1Byte(字节)

十进制(BCD码)调整指令

压缩BCD码运算:将压缩BCD数用二进制加、减指令(ADD,SUB,ADC,SBC)运算,运算结果必须用一下调整指令调整为压缩BCD数的结果

1
2
3
4
5
6
7
8
DAA; 将AL中的和调整为压缩BCD数
DAS; 将AL中的差调整为压缩BCD数
非压缩BCD字节数加减乘除(ASCII调整)
非压缩BCD码运算:将非压缩BCD数用二进制加减乘除指令运算,配合相应的调整。
AAA; (+)将AL中的和调整为非压缩BCD数
AAS; (-)将AL中的差调整为非压缩BCD数
AAM; (*)将AL中的积调整为非压缩BCD数
AAD; (/)调整AX中的被除数,相除的商即为非压缩BCD数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 两个ASCII数想减(7-5)
MOV AL, '7'; 37H
SUB AL, '5'; 37H-35H
AAS ; 调整,(AL)=02H
# 非组合BCD数6*8=48
MOV AL, O6H
MOV BL, 08H
MUL BL ; AX <- (AL)*(BL)
AAM ; 调整(AX)=0408H
# 非组合BCD数17/5=3余2
MOV AX, 0107H
MOV BL, 05H
AAD ; 调整被除数,(AX)=0011H
DIV BL ; 相除,(AL)=03,(AH)=02

x86

通用寄存器

x86架构(16位、32位、64位)有8个通用寄存器General-Purpose Registers (GPR),而64位又新增了8个通用寄存器

寄存器(16位) 英文名 中文名 主要功能
AX Accumulator register) 累加寄存器 算术运算
CX Counter register 计数寄存器 计算循环次数
DX Data register 数据寄存器 算术操作和I/O操作
BX Base register 基址寄存器 存储内存地址
SP Stack Pointer register 栈指针寄存器 指向栈顶
BP Stack Base Pointer register 基址指针寄存器 指向栈基址(栈底部)
SI Source Index register) 源基址寄存器 存储数据发送源的内存地址
DI Destination Index register 目标基址寄存器 存储数据发送目标的内存地址

32位增加前缀E意为extended

64位增加前缀R意为register

EAX ECX EDX EBX ESP EBP ESI EDI
RAX RCX RDX RBX RSP RBP RSI RDI

段寄存器

6个段寄存器

SS (Stack Segment) Pointer to the stack. 堆栈段
CS (Code Segment) Pointer to the code. 代码段
DS (Data Segment) Pointer to the data. 数据段
ES (Extra Segment) Pointer to extra data (‘E’ stands for ‘Extra’). 附加段
FS (F Segment) Pointer to more extra data (‘F’ comes after ‘E’). F段
GS (G Segment) Pointer to still more extra data (‘G’ comes after ‘F’). G段

64位新增8个通用寄存器R8-R15

R0-R7 => RAX-RDI

R8D-R15D 每个64位寄存器的低32位(D代表双字,32位)

R8W-R15W 低16位(W 字,16位)

R8B-R15B 低8位(B 字节,8位)

AX的高8位AH低8位AX

所以我们访问存储器有5种方式

RAX,EAX,AX,AH,AL

IP 指令指针(寄存器),存储代码段的偏移量,指向当前将要执行的指令

program counter (PC) 通常在Intel x86和Itanium微处理器中叫做IP(Instruction pointer),有时又叫做address register(IAR)

处理器从存储获取指令来执行,存储器的地址由pc而来,通常为顺序获取,每取一次,pc自增加。

有些控制指令可以改变pc的值而实现跳跃获取

函数调用过程寄存的角色

x86registers

AT&T vs Intel

x86 汇编语言两大主要派系: Intel stntax,AT&T syntax

Intel语法主要应用在DOS和Windows

AT&T语法主要应用在Unix

主要不同

AT&T Intel
符号 立即数要加前缀”$“,寄存器要加前缀”% 汇编器自行决定符号的类型
参数的顺序 源在前,目标操作数在后
mov $5, %eax
目标在前,源在后
mov eax, 5
参数带有尺寸 助记符带有后缀标明操作的内存大小
q:qwordl:long,dwordw:wordb:byte
addl $4, %esp
从寄存器的名字可推断出操作类型的大小
比如rax~q,eax~l,ax~w,al~b
add esp, 4
有效地址 DISP(BASE,INDEX,SCALE).
mov mem_location(%ebx,%ecx,4), %eax
[`]方括号里书序表达式,此外如果无法从操作数中推断出大小则需要添加关键字如qword,dword,word,byte<br />mov eax , [ebx + ecx*4 + mem_location]<br />mov eax. dword prt [ebp+8]`
1
2
mov eax. dword prt [ebp+8]; dword double word pointer
# 这句指令的意思:ebp寄存器的值加8后得到的值为内存地址,从改地址开始取4个字节的数据

名词理解

二进制负数运算

二进制数中表示负数值时,一般会把最高位作为符号来使用,因此我们把这个最高位称为符号位。符号位是 0 时表示正数 ,符号位 是 1 时表示负数。那么-1 用 8 位二进制数来表示的话是什么样的呢?可能很多人会认为“1 的二进制数是 00000001,因此-1 就是 10000001”,但这个答案是错的,正确答案是 11111111。

计算机在做减法运算时,实际上内部是在做加法运算。用加法运算来实现减法运算,是不是很新奇呢?为此,在表示负数时就需要使用“二进制的补数”。补数就是用正数来表示负数,很不可思议吧。

为了获得补数,我们需要将二进制数的各数位的数值全部取反,然后再将结果加 1。例如,用 8 位二进制数表示-1 时,只需求得 1,也就是 00000001 的补数即可。具体来说,就是将各数位的 0 取反成 1,1 取反成 0,然后再将取反的结果加 1,最后就转化成了 11111111

“当然,结果不为 0 的运算同样可以通过使用补数来得到正确的结果。不过,有一点需要注意,当运算结果为负数时,计算结果的值也是以补数的形式来表示的。比如 3-5 这个运算,用 8 位二进制数表示 3 时为 00000011,而 5 = 00000101 的补数为“取反+1”,也就是 11111011。因此 3-5 其实就是 00000011+11111011 的运算。

00000011 + 11111011 的运算结果为 11111110,最高位变成了 1。这就表示结果是一个负数,这点大家应该都能理解。那么 11111110 表示的负数是多少大家知道吗?这时我们可以利用负负得正这个性质。假若 11111110 是负△△,那么 11111110 的补数就是正△△。通过求解补数的补数,就可知该值的绝对值。11111110 的补数,取反加 1 后为 00000010。这个是 2 的十进制数。因此,11111110 表示的就是-2。

仔细思考一下补数的机制,大家就会明白像-32768~32767 这样负数比正数多一个的原因了。最高位是 0 的正数,有 0~32767 共 32768 个,这其中也包含 0。最高位是 1 的负数,有-1~-32768 共 32768 个,这其中不包含 0。也就是说,0 包含在正数范围内,所以负数就要比正数多 1 个。虽然 0 不是正数,但考虑到符号位,就将其划分到了正数中。

符号拓展位
符号扩充(又名符号扩展)

是计算机算术中,在保留数字的符号(正负性)及数值的情况下,增加二进制数字位数的操作。此操作根据使用的特定有符号数处理方式,通过在数字的最高有效位端添加位数的方式完成。

举个例子,若计算机使用六位二进制数表示数字“00 1010”(十进制的正10),且此数字需要将字长符号扩充至十六位,则扩充后的值为“0000 0000 0000 1010”。此时,数值与符号均保留了下来。

若计算机使用十位数及二补数表示数字“11 1111 0001”(十进制的负15),且此值需要扩充至十六位,则扩充后的值为“1111 1111 1111 0001”。 此时,负号及原数字数值通过将左侧填充为1的方式保留了下来。

在英特尔的x86指令集中,符号扩充有两种方式:

使用指令cbwcwdcwdecdq:分别将字节转化为字、字转化为双字、字转化为扩充双字、双字转化为四倍长字(x86环境中,一个字节为8位、一个字16位、双字与扩充双字均为32位、四倍长字64位);
使用符号扩展移动指令,可通过movsx(“带符号移动”)指令族完成。

零扩展

是与符号扩展类似的概念。在移动操作或转换操作中,零扩展指的是将目标的高位数设置为零,而不是将高位数设置成原数字的最高有效位。零扩展通常用于将无符号数字移动至较大的字段中,同时保留其数值;而符号扩展通常用于有符号的数字。

在x86及x64指令集中,movzx指令(“使用零扩展移动”)将执行零扩展移动。举个例子,movzx ebx, al会复制al中的一个字节至ebx的低位字节,随后使用0填充ebx的剩余字节。

在x64平台上,大多数写入通用寄存器的低32位的指令将使用0填充目标寄存器的上半部分。举个例子,指令mov eax, 1234 将清除rax寄存器的上32位。

移位运算

移位运算指的是将二进制数值的各数位进行左右移位

<<左移

>>右移

逻辑右移和算术右移的区别

右移有移位后在最高位补0和最高位补1两种情况

逻辑右移:移位后需要在最高位补 0。

当二进制数的值表示图形模式而非数值时,类似于霓虹灯往右滚动的效果。

算术右移:移位后要在最高位填充移位前符号位的值(0 或 1)

当二进制数作为带符号的数值进行运算时,如果数值是用补数表示的负数值,那么右移后在空出来的最高位补 1,如果是正数,只需在最高位补 0 即可。

PS 只有在右移时才必须区分逻辑位移和算术位移。左移时,无论是图形模式(逻辑左移)还是相乘运算(算术左移),都只需在空出来的低位补 0 即可

BCD码

压缩BCD码:4位二进制表示一个十进制位

需要注意的是每个十进制数都用一组四位二进制数来表示。不足4位者(十进制数0到7)加添0字开头,以凑足4位。还有六种数码:1010,1011,1100,1101,1110,1111,是不用的。

非压缩BCD码:一个字节(8位二进制位)表示一个十进制位

非压缩型BCD码一个字节可存放一个一位十进制数,其中高4位的内容不做规定(也有部分书籍要求为0,二者均可),低4位二进制表示该位十进制数。如5的非压缩型BCD码是0000 0101,必须存放在一个字节中,56的非压缩型BCD码是00000101 00000110,必须存放在一个字中;

参考资料

《微机原理与接口技术》 戴胜华

https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture

https://en.wikipedia.org/wiki/X86_memory_segmentation

https://wiki.cdot.senecacollege.ca/wiki/X86_64_Register_and_Instruction_Quick_Start

AT&T vs Intel