转自 《汇编语言程序设计》
一、通用寄存器
寄存器作用 | 64-bit register | Lower 32 bits | Lower 16 bits | Lower 8 bits |
---|---|---|---|---|
累加器 |
rax |
eax |
ax |
al |
基址寄存器 |
rbx |
ebx |
bx |
bl |
计数器 |
rcx |
ecx |
cx |
cl |
rdx |
edx |
dx |
dl |
|
存放源指针 |
rsi |
esi |
si |
sil |
存放目的指针 |
rdi |
edi |
di |
dil |
存放栈底指针 |
rbp |
ebp |
bp |
bpl |
存放栈顶指针 |
rsp |
esp |
sp |
spl |
r8 |
r8d |
r8w |
r8b |
|
r9 |
r9d |
r9w |
r9b |
|
r10 |
r10d |
r10w |
r10b |
|
r11 |
r11d |
r11w |
r11b |
|
r12 |
r12d |
r12w |
r12b |
|
r13 |
r13d |
r13w |
r13b |
|
r14 |
r14d |
r14w |
r14b |
|
r15 |
r15d |
r15w |
r15b |
注1:整数在寄存器中以大尾数的形式存放!
二、操作码后缀
- b = byte (8 bit)
- s = short (16 bit integer) or single (32-bit floating point)
- w = word (16 bit)
- l = long (32 bit integer or 64-bit floating point)
- q = quad (64 bit)
- t = ten bytes (80-bit floating point)
例如:movq、movl、movw、movb等
注意,使用的后缀一定要和寄存器相对应,例如 movl $0, %eax; movq $0, %rax; movb $0, %al
三、操作码
1. mov+suffix source, dest
通用的数据传送指令:
- 立即数传送到寄存器和内存
- movl $0, %eax
- movl $0, height # 将立即数 0 存放到 height 指向的内存地址
- 寄存器之间传送数据
- movl %eax, %ebx
- 在内存和寄存器之间传送数据
- movl value, %eax # 将value指向的内存地址的数据值传送个eax
- movl %ecx, value # 将ecx中的数据传送给value指向的内存地址
- 使用变址的内存位置,内存的位置由下列因素决定:基址,添加到基址上的偏移地址,数据元素的长度,确定选择哪个数据元素的变址。表达式的格式是:base_address(offset_address, index, size)。获取的数据值位于 base_address + offset_address + index * size。如果其中的任何值为0,就可以忽略它们(但是仍然需要逗号作为占位符)。offset_address和index必须是寄存器,size则可以是数字。 例如:movl values(, %edi, 4), %eax
- movl $values, %edi # 将values指向的内存地址存放到edi。寄存器间接寻址模式
- movl %ebx, (%edi) # 将ebx中的数据传送给edi中存放的内存地址。寄存器间接寻址模式
- movl %edx, 4(%edi) # 作用同上, 将ebx中的数据传送给edi中存放的内存地址之后4个字节的内存地址;注意,GNU不允许将值与寄存器相加,必须把值放在括号之外。寄存器间接寻址模式
- movl %edx, -4(%edi) # 作用同上,方向相反。
2. nop
空指令
3. cmov+flags source, dest
条件传送指令。mov指令有时会发生在特定的条件下,为了避免使用jmp指令,因此cmov被引入。
例如cmova代表“大于”,那么cmova %eax, %ebx 表示当eax存放的值大于ebx存放的值时,才会将exa中的值传送到ebx中。
---恢复内容结束---
一、通用寄存器
寄存器作用 | 64-bit register | Lower 32 bits | Lower 16 bits | Lower 8 bits |
---|---|---|---|---|
累加器 |
rax |
eax |
ax |
al |
基址寄存器 |
rbx |
ebx |
bx |
bl |
计数器 |
rcx |
ecx |
cx |
cl |
rdx |
edx |
dx |
dl |
|
存放源指针 |
rsi |
esi |
si |
sil |
存放目的指针 |
rdi |
edi |
di |
dil |
存放栈的基址指针 |
rbp |
ebp |
bp |
bpl |
存放栈顶指针 |
rsp |
esp |
sp |
spl |
r8 |
r8d |
r8w |
r8b |
|
r9 |
r9d |
r9w |
r9b |
|
r10 |
r10d |
r10w |
r10b |
|
r11 |
r11d |
r11w |
r11b |
|
r12 |
r12d |
r12w |
r12b |
|
r13 |
r13d |
r13w |
r13b |
|
r14 |
r14d |
r14w |
r14b |
|
r15 |
r15d |
r15w |
r15b |
二、操作码后缀
- b = byte (8 bit)
- s = short (16 bit integer) or single (32-bit floating point)
- w = word (16 bit)
- l = long (32 bit integer or 64-bit floating point)
- q = quad (64 bit)
- t = ten bytes (80-bit floating point)
例如:movq、movl、movw、movb等
注意,使用的后缀一定要和寄存器相对应,例如 movl $0, %eax; movq $0, %rax; movb $0, %al
三、操作码
1. mov+suffix source, dest
通用的数据传送指令:
- 立即数传送到寄存器和内存
- movl $0, %eax
- movl $0, height # 将立即数 0 存放到 height 指向的内存地址
- 寄存器之间传送数据
- movl %eax, %ebx
- 在内存和寄存器之间传送数据
- movl value, %eax # 将value指向的内存地址的数据值传送个eax
- movl %ecx, value # 将ecx中的数据传送给value指向的内存地址
- 使用变址的内存位置,内存的位置由下列因素决定:基址,添加到基址上的偏移地址,数据元素的长度,确定选择哪个数据元素的变址。表达式的格式是:base_address(offset_address, index, size)。获取的数据值位于 base_address + offset_address + index * size。如果其中的任何值为0,就可以忽略它们(但是仍然需要逗号作为占位符)。offset_address和index必须是寄存器,size则可以是数字。 例如:movl values(, %edi, 4), %eax
- movl $values, %edi # 将values指向的内存地址存放到edi。寄存器间接寻址模式
- movl %ebx, (%edi) # 将ebx中的数据传送给edi中存放的内存地址。寄存器间接寻址模式
- movl %edx, 4(%edi) # 作用同上, 将ebx中的数据传送给edi中存放的内存地址之后4个字节的内存地址;注意,GNU不允许将值与寄存器相加,必须把值放在括号之外。寄存器间接寻址模式
- movl %edx, -4(%edi) # 作用同上,方向相反。
2. nop
空指令
3. cmov+flags source, dest
条件传送指令。mov指令有时会发生在特定的条件下,为了避免使用jmp指令,因此cmov被引入。
例如cmova代表“大于”,那么cmova %eax, %ebx 表示当eax存放的值大于ebx存放的值时,才会将exa中的值传送到ebx中。
4. xchg operand1, operand2
在两个寄存器或寄存器与内存位置之间交换数据值
- 不能同时都为内存操作数
- 任何一个操作数都不能为段寄存器
- 任何一个操作数不能为立即数
- 两个操作数的长度不能不相等
当一个operand是内存位置时,处理器的LOCK信号被自动标明,防止在交换过程中任何其他处理器访问这个内存位置;LOCK处理是非常耗时的。
5. bswap operand1
寄存器内的字节次序变反。注意,位的顺序没有被反转,被反转的是寄存器中包含的各个字节。这个指令可以完成 little endian 和 big endian 之间的切换。
6. xadd source, dest
将source和dest中的值交换,交换之后相加,结果存在目标操作数dest中。其中source必须是寄存器,dest可以是寄存器,也可以是内存位置
7. cmpxchg source, dest
比较目标操作数和RAX、EAX、AX或AL寄存器,如果两个值相等,就把源操作数的值加载到目标操作数中;如果两个值不相等,就把目标操作数加载到RAX、EAX、AX或AL寄存器
8. push+suffix source / pop+suffix dest
压栈/弹栈操作,在64位系统下,好像只支持pushw和pushq;在32位系统下,只支持pushw和pushl。pop也一样。
9. 无条件分支指令
1). 跳转—— jmp location
location 是要跳转的内存地址,这个值被声明为程序代码中的标签:
movl %eax, %ebx addl %ebx, %ecx jmp end movl $1, %eax end: movl $10, %ebx
jmp指令把指令指针(EIP)的值改为jmp指令中指定的内存位置。
在幕后,单一汇编跳转指令被汇编位跳转操作码的3中不同类型之一:
- 短跳转:跳转偏移量小于128字节时
- 近跳转:其他情况
- 远跳转:在分段内存模式下,当跳转到另一个段中的指令时
2). 调用—— call address
与jmp的操作数相同,address操作数引用程序中的标签。
调用与跳转的不同之处在于,它保存发生跳转的位置,并在需要的时候返回这个位置;用于实现函数。
函数的最后一条指令是返回指令ret,它没有操作数;通过查看栈,它知道应该返回到什么位置。
3). 中断
终端包括两种形式:
- 硬中断:硬件中断发出信号,表示硬件层发生的事件(比如I/O端口接收到输入信号)
- 软中断:一般用于系统调用。
10. 条件分支指令
不同与无条件分支指令,条件分支不总是被执行。条件分支的结果取决于执行分支时EFLAGS寄存器的状态。EFLAGS寄存器中有很多位,但是条件分支只和其中的五位有关:
- 进位标志 CF
- 溢出标志 OF
- 奇偶校验标志 PF
- 符号标志 SF
- 零标志 ZF
每个条件跳转指令都检查特定的标志位以便确定是否符合进行跳转的条件。使用这五个不同的标志位,可以执行几种跳转组合。
条件跳转指令能够的格式是:
j+flag address
其中flag是1个到3个字符的条件代码,address是程序要跳转到的位置(通常以标签表示)。可用的条件跳转指令很多,就不再一一列出。为了使用条件跳转,在进行跳转之前,必须进行设置EFLAGS寄存器的操作。下一步介绍几个使用条件跳转的例子。
1). 比较指令 cmp operand1, operand2
cmp可以翻译成“operand2比operand1”,例如:
movl $15, %eax movl $10, %ebx cmp %eax, %ebx jge function # jge 代表如果大于或等于,则跳转
由于ebx寄存器的值小于eax寄存器的值,因此不会执行跳转。
2). 使用标志位的范例
下面介绍每个标志位如何影响条件跳转。
A. 使用零标志
如果零标志被置1(两个操作数相等),JE(如果相等,则跳转)和JZ(如果为0,则跳转)就跳转到分支。零标志可以由cmp指令设置,也可以由计算结果为0的数学指令设置,例如:
movl $30, %eax subl $30, %eax jz function
B. 使用溢出标志
溢出专门用在处理带符号数字时。当带符号值对于包含它的数据元素来说太大是,溢出标志被设置为1。这经常发生在溢出了保存数据的寄存器长度的数学操作的过程中。对应的跳转指令为JO(如果溢出,则跳转)。
C. 使用奇偶校验标志
奇偶校验标志表明数学运算答案中应该为1的位的数目。如果结果中被设置为1的位的数目是偶数,则设置奇偶校验位(置1)。如果设置为1的位的数目是奇数,则设置奇偶校验位(置0)。
D. 使用符号标志
符号标志用在带符号数中,用于表示寄存器中包含的值的符号改变。在带符号数中,最后一位(最高位)用作符号位。它表明数字表示是负值(置1)还是正值(置0)。
E. 使用进位标志
进位标志用在数学表达式中,表示无符号数中何时发生溢出。当指令导致寄存器超出其数据长度限制时设置进位标志;当无符号值小于0时也会设置进位标志。
11. 循环
循环使用ecx寄存器作为计数器并且随着循环指令的执行自动递减它的值。下表介绍循环系列中的指令:
指令 | 描述 |
LOOP | ECX寄存器不为0时才执行循环 |
LOOPE/LOOPZ | ECX寄存器不为0并且ZF标志位为1时才执行循环 |
LOOPNE/LOOPNZ | ECX寄存器不为0并且ZF标志位为0时才执行循环 |
循环指令的格式是
loop address
其中address是要跳转到的程序代码位置的标签名称。注意,循环指令只支持8位偏移量,所以只能进行短跳转。
循环开始之前必须在ecx寄存器中设置执行迭代的次数值:
< code before the loop > movl $100, %ecx loop1: < code to loop through > loop loop1 < code after the loop >
要注意循环内部代码,如果ecx寄存器被修改了,就会影响循环的操作。在循环内实现函数调用时要格外谨慎,因为函数可能很容易地在程序员无意识的情况下破坏ecx寄存器的值。
除此之外,还要防止loop灾难。当执行loop指令时,它首先把ecx中的值递减1,然后检查ecx中的值是否为0。使用这个逻辑,如果在loop指令之前ecx的值已经为0,loop指令会把它递减1,使它成为-1,因为这个值非0,所以loop指令继续执行下去,循环回到定义标签。循环最终会在寄存器溢出时退出,并且显示错误的值。解决方法是使用条件分支指令JCXZ(如果CX寄存器为0,则跳转)。
12. add+suffix source, dest
source可以是立即值、内存位置或寄存器,dest可以是寄存器或者内存位置;但source和dest不能同时为内存位置;加法的结果存放在dest中。
13. sub+suffix source, dest
从dest的值减去source的值,结果存储在dest中;但source和dest不能同时为内存位置
14. inc dest / dec dest
分别用于对无符号整数值进行递增和递减。两条指令都不会影响进位标志。dest可以是寄存器或内存中的值。
如果对设置为0的32位寄存器进行递减,新的值将是0xffffffff
15. mul+suffix source
用于两个无符号整数相乘。source可以是内存或寄存器中的值。指令中只有源操作数,而目标操作数是隐含的;根据源操作数的值的长度,目标操作数必须存放在AL、AX、EAX或RAX寄存器中。由于乘法可能产生很大的值,因此mul指令的目标位置必须是源操作数的两倍长度,例如,如果源值是8位,那么目标操作数就是AX寄存器,当源操作数更大时,情况会更加复杂。
16. imul+suffix source
mul指令只能用于无符号整数,imul指令可以用于带符号和无符号整数,但是必须小心目标结果不使用目标的最高有效位。对于较大的值,imul指令只对带符号整数是合法的。
imul由三种不同的指令格式:
- imul source:其行为与mul完全一样
- imul source, dest:source可以是内存或寄存器中的值,dest必须是通用寄存器
- imul multiplier, source, dest:其中multiplier是一个立即数,source是寄存器或内存中的值,dest必须是通用寄存器。multiplier与source相乘后将结果存放在dest中
17. div+suffix divisor
用于无符号整数的除法。divisor可以是内存或寄存器中的值,它作为除数。被除数要存放在AL、AX、EAX或RAX寄存器,最终得到的商和余数则根据操作数的位数存放在EAX和EDX中。
18. idiv+suffix divisor
idiv指令的使用方式和div指令完全一样,但是它用于带符号整数的除法操作。和imul指令不同,idiv指令只有一种格式。对于带符号整数的除法,余数的符号总是与被除数的符号相同。
19. sal+suffix 和 shl+suffix
向左移位,sal(向左算数移位)指令和shl(向左逻辑移位)指令的效果一样。它们有三种不同格式:
- sal dest:dest的值向左移一位
- sal %cl, dest:dest的值向左移动CL寄存器中指定的位数
- sal shifter, dest:dest的值向左移动shifter值指定的位数
在所有格式中,目标操作数可以是8、16、32位寄存器或内存。
移位造成的空位由0填充,移位造成的超出数据长度的任何位首先被存放在进位标志中,然后在下一次移位操作中被丢弃。因此,如果值的最高有效位为1,经过2次向左移位操作之后,最高有效位就会从进位标志中丢弃。
20. sar+suffix 和 shr+suffix
与向左移位不同,向右移位需要考虑符号。对于shr指令,它只能用于无符号整数的移位;对于sar指令,它会根据整数的符号,在空位填充1(负数)或0(正数)。和向右移位相同,移出的最低有效位也会先被存放在进位标志,等到下次移位时再被丢弃。
21. 循环移位
- rol+suffix:向左循环移位
- ror+suffix:向右循环移位
- rcl+suffix:向左循环移位,并且包含进位标志
- rcr+suffix:向右循环移位,并且包含进位标志
最后两条指令使用进位标志作为附加位的位置,来支持9位移位,循环指令与移位指令相同,存在三种指令格式。
23. 布尔操作
- and
- not
- or
- xor
and,or以及xor指令使用相同的格式:
and source, dest
其中source可以是8、16、32位立即数、寄存器或内存中的值,dest可以是8、16、32位寄存器或内存中的值(和以往一样,不能同时使用内存值作为源和目标);not指令使用单一操作数,它既是源,也是目标结果的位置。
24. C/C++ 内联汇编
1). 内联汇编格式
gcc使用asm关键字指出使用汇编语言编写的源代码段落。asm段的基本格式如下:
asm("assembly code\n");
包含在括号中的汇编代码必须按照特定的格式:
- 指令必须写在引号里
- 如果包含的指令超过一条,那么必须使用换行符分隔汇编代码的每一行。需要这条规则是因为编译器逐字地取得asm段中的汇编代码,并且把它们放在为程序生成的汇编代码中。每一条汇编指令都必须在单独的一行中——因此需要换行符。
还可以将指令放在单独的行中,每条指令都必须括在引号里:
asm("assembly code 1\n"
"assembly code 2\n");
2). 使用全局C变量
内联汇编代码可以利用程序中定义的全局C变量,记住,必须是全局变量。通过C程序中使用的相同名称引用这种变量。
3). 使用volatile修饰符
为了避免编译器对内联汇编代码的优化,可以使用volatile修饰符:
asm volatile("assembly code\n")
25. 扩展asm
扩展的asm格式提供附加的特性,因此必须采用新的格式:
asm("assembly code" : output locations : input operands : changed registers);
这种格式由4部分构成,使用冒号分隔:
- 汇编代码:使用和基本asm格式相同的语法的内联汇编代码
- 输出位置:包含内联汇编代码的输出值的寄存器和内存位置列表
- 输入操作数:包含内联汇编代码的输入值的寄存器和内存位置的列表
- 改动的寄存器:内联代码改变的任何其他寄存器的列表
在扩展asm格式中,并不是所有这些部分都必须出现。如果汇编代码不生成输出值,这个部分就必须为空,但是必须使用两个冒号把汇编代码和输入操作数分隔开。如果内联汇编代码不该动寄存器的值,那么可以忽略最后的冒号。