目录
1.加载有效地址
2. 整数运算指令
2.1 INC 和 DEC
2.2 NEG
2.3 ADD、SUB 和 IMUL
3. 布尔指令
3.1 AND
3.2 OR
3.3 XOR
3.4 NOT
4. 移位操作
4.1 算术左移和逻辑左移
4.2 算术右移和逻辑右移
5. 特殊的算术操作
1.加载有效地址
指令 | 效果 | 描述 |
leaq S, D | D ← &S | 加载有效地址 |
加载有效地址( load effective address )指令leaq是movq指令的变形,在64位系统下地址长度为64位,因此lea指令的大小后缀为q,没有其他变种,其目标操作数必须是一个寄存器
leaq指令非常特别,它的一般格式是 leaq (寄存器) 寄存器,看上去像是从内存中读取数据到寄存器,实际上leaq从不发生内存引用,也就是说leaq指令不访问内存,以下面的程序来说明
int main()
{
int x = 10;
int *ptr = &x;
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp)
4004f8: 48 8d 45 f4 lea -0xc(%rbp),%rax // 取a的地址放进%rax
4004fc: 48 89 45 f8 mov %rax,-0x8(%rbp)
400500: b8 00 00 00 00 mov $0x0,%eax
400505: 5d pop %rbp
400506: c3 retq
400507: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
40050e: 00 00
4004f8: 48 8d 45 f4 lea -0xc(%rbp),%rax
这条指令在mov指令中表示:以%rbp内存放的值作为基地址,加上偏移量0xc作为地址,再取这个地址处的数据,并将数据传送到寄存器%rax内
但在leaq指令则表示:以%rbp内存放的值作为基地址,加上偏移量0xc作为地址,将这个地址传送到寄存器内,也就是 &x 这一行为
%rbp是帧寄存器,保存着main函数栈帧的栈顶位置
假设%rbp的值是10000,地址1000c处保存的值是10
- movq指令将10传送到寄存器%rax
- leaq指令将1000c传送到寄存器%rax
leaq指令完成简单的基地址和偏移量的相加,实际上,leaq指令不光能完成地址的相加,也常用于普通的算术操作,比如下面一条指令
leaq 7(%rdx, %rdx, 4), %rax
假设寄存器%rdx的值为x,这条指令的意思是将 %rax的值设置成 x + 4x +7 ,参考linux下的寻址模式的计算,比如下面的代码
long scale(long x, long y, long z)
{
long t = x + 4 * y + 12 * z;
return t;
}
/*
long scale(long x, long y, long z)
x in %rdi, y in %rsi, z in %rdx
*/
scale:
leaq (%rdi,%rsi,4), %rax x + 4*y
leaq (%rdx,%rdx,2), %rdx z + 2*z = 3*z
leaq (%rax,%rdx,4), %rax (x+4*y) + 4*(3*z) = x + 4*y + 12*z
ret
因此,leaq指令也能完成加法和有限的乘法计算,需要注意的一点是,寻址模式中的比例因子只能是1,2,4,8,说明leaq指令完成乘法时,也只能与1,2,4,8相乘,上面代码中,第二行不能使用 leaq(%rax, %rdx, 12) 一步完成计算,而是要分成两步也正是因为这个原因
2. 整数运算指令
指令 | 效果 | 描述 |
INC D | D ← D + 1 | 加1 |
DEC D | D ← D - 1 | 减1 |
NEG D | D ← - D | 取负 |
NOT D | D ← ~ D | 取补 |
ADD S, D | D ← D + S | 加 |
SUB S, D | D ← D - S | 减 |
IMUL S, D | D ← D * S | 乘 |
这些整数操作随着操作数大小的不同在使用时要加上操作数大小描述符,因而有四种不同的指令
前四条指令inc,dec,neg 和 not的操作数都只有一个,即是源又是目的,因此称为一元操作,这个操作数可以是一个寄存器,也可以是一个内存位置
后三条指令add,sub,imul 的操作数有两个,其中第二个操作数即作为源使用,又作为目的使用,因此称为二元操作
2.1 INC 和 DEC
INC(Increment)指令从操作数中加1,DEC(Decrement)指令从操作数中减1,二者均不影响CF
指令格式
- inc reg/mem
- dec reg/mem
使用下面的代码查看inc和dec指令的功能
#include <stdio.h>
int main() {
int x = 10;
// printf("The value of x before the increment: %d\n", x); 10
__asm__ ( "inc %0\n" : "=r" (x) : "0" (x) );
// printf("The value of x after the increment: %d\n", x); 11
// printf("The value of x before the increment: %d\n", x); 11
__asm__ ( "dec %0\n" : "=r" (x) : "0" (x) );
// printf("The value of x after the increment: %d\n", x); 10
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: ff c0 inc %eax // 把%eax中的值加1
4004fd: 89 45 fc mov %eax,-0x4(%rbp)
400500: 8b 45 fc mov -0x4(%rbp),%eax
400503: ff c8 dec %eax // 把%eax中的值减1
400505: 89 45 fc mov %eax,-0x4(%rbp)
400508: b8 00 00 00 00 mov $0x0,%eax
40050d: 5d pop %rbp
40050e: c3 retq
40050f: 90 nop
2.2 NEG
NEG(negative):将数字转换为对应的二进制补码, 从而求得其相反数,影响的标志位有
进位标志CF、零标志ZF、符号标志SF、溢出标志OF、辅助进位标志AF和奇偶标志PF(结果低8位中,数值1 的个数是否为偶数)。
指令格式
- neg reg
- neg mem
使用下面的代码查看neg指令的功能
int main()
{
int i = 10;
i = -i;
i = ~i;
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
4004f8: f7 5d fc negl -0x4(%rbp) // i = -i;
4004fb: b8 00 00 00 00 mov $0x0,%eax
400500: 5d pop %rbp
400501: c3 retq
400502: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400509: 00 00 00
40050c: 0f 1f 40 00 nopl 0x0(%rax)
2.3 ADD、SUB 和 IMUL
ADD(addition):指令将同尺寸的源操作数和目的操作数相加
SUB(subtraction):指令将同尺寸的源操作数和目的操作数相减
IMUL(multiplication):指令将同尺寸的源操作数和目的操作数相乘
指令格式
- add 源操作数, 目的操作数
- sub 源操作数, 目的操作数
- imul 源操作数, 目的操作数
指令的源操作数可以是:立即数,寄存器,内存位置
指令的目的操作数可以是:寄存器,内存位置
使用下面的代码查看add,sub,和 imul 指令的功能
int main()
{
int a = 10;
int b = a + 10;
int c = b - 15;
a = a * b;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) // int a = 10;
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: 83 c0 0a add $0xa,%eax // int b = a + 10;
4004fe: 89 45 f8 mov %eax,-0x8(%rbp)
400501: 8b 45 f8 mov -0x8(%rbp),%eax
400504: 83 e8 0f sub $0xf,%eax // int c = b - 15;
400507: 89 45 f4 mov %eax,-0xc(%rbp)
40050a: 8b 45 fc mov -0x4(%rbp),%eax
40050d: 0f af 45 f8 imul -0x8(%rbp),%eax // a = a * b;
400511: 89 45 fc mov %eax,-0x4(%rbp)
400514: 5d pop %rbp
400515: c3 retq
400516: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40051d: 00 00 00
3. 布尔指令
C语言里存在位操作符
分别对应下面的指令
指令 | 效果 | 描述 |
AND S, D | D ← D & S | 与 |
OR S, D | D ← D | 1 | 或 |
XOR S, D | D ← D ^ S | 异或 |
NOT D | D ← ~ D | 取补 |
3.1 AND
AND 指令在每对操作数的对应数据位之间执行布尔位“与”操作,并将结果存放在目的操作数中
指令格式
- AND reg/mem/imm, reg/mem
AND指令总是使得CF=0、OF=0,并依据目的操作数的值修改SF、ZF和PF的值
参考如下代码
int main()
{
int x = 10; // 00000000 00000000 00000000 00001010
int y = x & 8; // 00000000 00000000 00000000 00001000
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) // int x = 10;
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: 83 e0 08 and $0x8,%eax // int y = x & 8;
4004fe: 89 45 f8 mov %eax,-0x8(%rbp)
400501: b8 00 00 00 00 mov $0x0,%eax
400506: 5d pop %rbp
400507: c3 retq
400508: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40050f: 00
3.2 OR
OR 指令在每对操作数的对应数据位之间执行布尔位“或” 操作,并将结果存放在目的操作数中
指令格式
- OR reg/mem/imm, reg/mem
OR指令总是使得CF=0、OF=0,依据目的操作数的值修改SF、ZF和PF的值
参考如下代码
int main()
{
int x = 10; // 00000000 00000000 00000000 00001010
int y = x | 8; // 00000000 00000000 00000000 00001000
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) // int x = 10;
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: 83 c8 08 or $0x8,%eax // int y = x | 8
4004fe: 89 45 f8 mov %eax,-0x8(%rbp)
400501: b8 00 00 00 00 mov $0x0,%eax
400506: 5d pop %rbp
400507: c3 retq
400508: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40050f: 00
3.3 XOR
XOR 指令在每对操作数的对应数据位之间执行布尔位“异或” 操作,并将结果存放在目的操作数中
指令格式
- XOR reg/mem/imm, reg/mem
OR指令总是使得CF=0、OF=0,依据目的操作数的值修改SF、ZF和PF的值
参考如下代码
int main()
{
int x = 10; // 00000000 00000000 00000000 00001010
int y = x ^ 8; // 00000000 00000000 00000000 00001000
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) // int x = 10;
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: 83 f0 08 xor $0x8,%eax // int y = x ^ 8;
4004fe: 89 45 f8 mov %eax,-0x8(%rbp)
400501: b8 00 00 00 00 mov $0x0,%eax
400506: 5d pop %rbp
400507: c3 retq
400508: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40050f: 00
3.4 NOT
NOT 指令将一个操作数的所有数据位取反
指令格式
- NOT reg/mem
NOT 指令不修改任何状态标志
参考如下代码
int main()
{
int x = 10; // 00000000 00000000 00000000 00001010
int y = ~x; // 11111111 11111111 11111111 11110101
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) // int x = 10;
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: f7 d0 not %eax // ~x;
4004fd: 89 45 f8 mov %eax,-0x8(%rbp) // int y = ~x;
400500: b8 00 00 00 00 mov $0x0,%eax
400505: 5d pop %rbp
400506: c3 retq
400507: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
40050e: 00 00
4. 移位操作
C语言中移位操作符分为左移操作符(<<)和右移操作符(>>),而移位操作又分左移和右移
指令 | 效果 | 描述 |
SAL k, D | D ← D << k | 算术左移 |
SHL k, D | D ← D << k | 逻辑左移 (等同SAL) |
SAR k, D | D ← D >> k | 算数右移 |
SHL k, D | D ← D >> k | 逻辑右移 |
移位操作
- 第一个操作数是移位量k,也就是二进制位移动的位数
- 第二个操作数是要移位的数
注意:移位量可以是一个立即数,或者放在单字节寄存器%cl中(规定了只能放在这里)
%cl长8位,可表示0~255,因此移位量的最大可以达到255位,但是显然没有这么长的数据类型,因此实际上移位操作是根据要移动的数的位数来决定取%cl的哪些值的,
x86-64中,移位操作对w位长的数据值进行操作,移位量是由%cl寄存器的低m位决定的,这里2的m次方等于w,高位会被忽略
比如此时%cl内是0xFF
%cl | 1111 1111 |
对于不同的数据类型
- char类型的数据,长8位,取%cl中的低三位 111,因此会移动7位
- short类型的数据,长16位,取%cl中低四位 1111,因此会移动15位
- int类型的数据,长32位,取%cl中低五位 11111,因此会移动31位
4.1 算术左移和逻辑左移
SAL(Arithmetic Left Shift):对目的操作数执行逻辑左移操作,低位填0 ,移出的最高位送CF
SHL(Logic Left Shift):与SAL指令等价
指令格式
- sal imm8/CL, reg/mem
- shl imm8/CL, reg/mem
参考如下代码
int main()
{
int x = 10; // 00000000 00000000 00000000 00001010
int y = x << 2; // 00000000 00000000 00000000 00101000
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) // int x = 10;
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: c1 e0 02 shl $0x2,%eax // x << 2
4004fe: 89 45 f8 mov %eax,-0x8(%rbp) // int y = x << 2;
400501: b8 00 00 00 00 mov $0x0,%eax
400506: 5d pop %rbp
400507: c3 retq
400508: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40050f: 00
4.2 算术右移和逻辑右移
SHR(Logic Shift Right ):对目的操作数执行逻辑右移操作,移出的数据位以0 填充,最低位被送到CF中
指令格式
- shr imm8/CL, reg/mem
SAL(Arithmetic Right Shift):用最高位填充空出的位,最低位拷贝至CF
指令格式
- sar imm8/CL, reg/mem
参考如下代码(这里算术右移最特殊,只演示算术右移)
int main()
{
int x1 = 10; // 00000000 00000000 00000000 00001010
int y1 = x1 >> 2; // 00000000 00000000 00000000 00000010
int x2 = -10; // 11111111 11111111 11111111 11110110
int y2 = x2 >> 2; // 11111111 11111111 11111111 11111101
return 0;
}
00000000004004ed <main>:
4004ed: 55 push %rbp
4004ee: 48 89 e5 mov %rsp,%rbp
4004f1: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp)
4004f8: 8b 45 fc mov -0x4(%rbp),%eax
4004fb: c1 f8 02 sar $0x2,%eax // 算术右移,以0填充
4004fe: 89 45 f8 mov %eax,-0x8(%rbp)
400501: c7 45 f4 f6 ff ff ff movl $0xfffffff6,-0xc(%rbp)
400508: 8b 45 f4 mov -0xc(%rbp),%eax
40050b: c1 f8 02 sar $0x2,%eax // 算术右移,以1填充
40050e: 89 45 f0 mov %eax,-0x10(%rbp)
400511: b8 00 00 00 00 mov $0x0,%eax
400516: 5d pop %rbp
400517: c3 retq
400518: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40051f: 00
由于算术右移会对有符号数和无符号数进行区分,因此使用算术右移对补码进行操作可以代替一部分的整数运算,对于下面的arith函数
long arith(long x, long y, long z)
{
long t1 = x ^ y;
long t2 = z * 48;
long t3 = t1 & 0x0F0F0F0F;
long t4 = t2 - t3;
return t4;
}
对应的汇编为
/*
long arith(long x, long y, long z)
x in %rdi, y in %rsi, z in %rdx
*/
arith:
xorq %rsi, %rdi t1 = x ^ y
leaq (%rdx,%rdx,2), %rax 3*z
salq $4, %rax t2 = 16 * (3*z) = 48*z
andl $252645135, %edi t3 = t1 & 0x0F0F0F0F
subq %rdi, %rax Return t2 - t3
ret
这里用salq $4, %rax 代替乘法,可以加快运算
5. 特殊的算术操作
两个64位有符号数或者无符号数相乘得到的乘积需要128位来表示。x86-64指令集对128位数的操作提供了一定程度上的支持,Intel将16字节的数称为8字(oct word)
下表是支持产生两个64位数字的全128位乘积以及整数除法的指令
指令 | 效果 | 描述 |
imuq S | R[ %rdx ]:R[ %rax ] ← S × R[ %rax ] | 有符号乘法 |
mulq S | R[ %rdx ]:R[ %rax ] ← S × R[ %rax ] | 无符号乘法 |
cqto | R[ %rdx ]:R[ %rax ] ← SignExtend(R[ %rax ]) | 转化为八字 |
idivq S | R[ %rdx ] ← R[ %rdx ]:R[ %rax ]mod S R[ %rax ] ← R[ %rdx ]:R[ %rax ]÷ S | 有符号除法 |
divq S | R[ %rdx ] ← R[ %rdx ]:R[ %rax ]mod S R[ %rax ] ← R[ %rdx ]:R[ %rax ]÷ S | 无符号除法 |
两个寄存器%rdx(64位)和%rax(64位)组成一个128位的八字,根据乘积中高部分是否为0设置或清楚CF、OF
对于无符号乘法(mulq)和有符号乘法(imulq)而言,二者都是单操作数乘法指令,都需要将一个参数存放在寄存器%rax中,而另一个则作为指令的源操作数给出,乘积放在寄存器%rdx和%rax中
%rdx(64位) | %rax(64位) |
下面是一个示例,其中细节见CASPP原书3.5.5小节
#include <inttypes.h>
typedef unsigned __int128 uint128_t;
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y)
{
*dest = x * (uint128_t)y;
}
/*
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y)
dest in %rdi, x in %rsi, y in %rdx
*/
store_uprod:
movq %rsi, %rax Copy x to multiplicand
mulq %rdx Multiply by y
movq %rax, (%rdi) Store lower 8 bytes at dest
movq %rdx, 8(%rdi) Store upper 8 bytes at dest+8
ret