3.2 程序编码
【例】Linux可以用命令行将源文件编译为可执行程序
gcc -Og -o p p1.c p2.c
- 源代码转换为可执行代码的过程
- 源代码(p1.c和p2.c):通过编译器生成汇编代码
- 汇编代码(p1.s和p2.s):通过汇编器转化成二进制目标代码文件
- 目标代码(p1.o和p2.o):通过链接器将目标代码合并为可执行文件p
【注】-Og代表原始C代码整体结构的机器代码的优化等级。如果要提高程序性能,可以使用较高的优化等级-O1或-O2,但是会破坏C代码的结构
3.2.2 代码示例
long mult2(long, long);
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}
【编译】gcc -Og -S mstore.c,生成AT&T格式的汇编代码mstore.s。去掉伪指令后如下:
// void multstore(long x, long y, long *dest)
// x in %rdi, y in %rsi, dest in %rdx
multstore:
pushq %rbx // Save %rbx
movq %rdx, %rbx // Copy dest to %rbx
call mult2 // Call mult2(x, y)
movq %rax, (%rbx) // mult2的返回值是%rax,赋值到*dest中
popq %rbx // Restore %rbx
ret // return
【汇编】gcc -Og -c mstore.c,生成mstore.o。内部有一段代码对应目标代码
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3
【反汇编】objdump -d mstore.o。可以看到与编译的结果是类似的。区别:省略了一些指令的后缀'q',call和ret加上了后缀'q'(其实省略掉也没问题)
0000000000000000 <multstore>:
0: 53 push %rbx
1: 48 89 d3 mov %rdx,%rbx
4: e8 00 00 00 00 callq 9 <multstore+0x9>
9: 48 89 03 mov %rax,(%rbx)
c: 5b pop %rbx
d: c3 retq
3.3 数据格式
汇编代码的指令处理,某些指令是带后缀的,如movb、movq。
- 整型:b代表字节(8bit);w代表字(16bit);l代表双字(32bit);q代表四字(64bit)
- 浮点型:有不同的指令,s代表单精度(4字节);l代表双精度(8字节)
3.4 访问信息
通用寄存器:x86-64的CPU包含16个存储64位值的通用目的寄存器
- 寄存器的低位部分可以独立使用,兼容历史指令
- 不同的寄存器扮演不同的角色,比如
%rax默认作为返回值
3.4.1 操作数
AT&T汇编指令格式:操作码 + 操作数
movq (%rdi), %rax
addq $8, %rbx
操作数分为三种类型:
- 立即数:表示常数。格式是
$数字,比如$-577、$0x1F - 寄存器:表示某个寄存器的值,格式是
%寄存器,比如%rax - 内存引用:相当于指针,根据地址访问某个内存的位置
- 表示方式:,分为4个部分:立即数偏移、基址寄存器、变址寄存器、比例因子(必须是1、2、4、8之一)
- 有效地址:
【例】书中例子:假设寄存器和内存地址的值如下
那么所示操作数的值如下:
3.4.2 数据传送指令——MOV
mov:将数据从源位置复制到目的位置。根据操作数据大小,分为四种情况:movb、movw、movl、movq
movl $0x4050, %eax //4 bytes
movw %bp, %sp //2 bytes
movb (%rdi, %rcx), %al //1 byte
movb $-17, (%rsp) //1 byte
movq %rax, -12(%rbp) //8 bytes
【例】mov立即数
movabsq $0x0011223344556677, %rax //%rax = 0011223344556677
movb $-1, %al //%rax = 00112233445566FF
movw $-1, %al //%rax = 001122334455FFFF
movl $-1, %al //%rax = 00000000FFFFFFFF
movq $-1, %al //%rax = FFFFFFFFFFFFFFFF
【注】
- 使用
movl时,会把寄存器的高4位字节设置为0 - 常规的
movq只能以32位补码作为源立即数。64位立即数要用movabsq,且只能以寄存器作为目的位置
- 移动指令:较小的源复制到较大的目的时使用
movz:零扩展,剩余位填充0(2.2.6节零扩展)。如:movzbw表示byte字节领扩展到wordmovs:符号扩展,剩余位按符号扩展填充(2.2.6节符号扩展)cltq:相当于movslq %eax, %rax- 注意:movzlq不存在,原因是与movl相同
【例】书中例子
movabsq $0x0011223344556677, %rax //%rax = 0011223344556677
movb $0xAA, %dl //%dl = AA
movb %dl, %al //%rax = 00112233445566AA
movsbq %dl, %rax //%rax = FFFFFFFFFFFFFFAA
movzbq %dl, %rax //%rax = 00000000000000AA
3.4.3 C语言的mov
long exchange(long *xp, long y)
{
long x = *xp;
*xp = y;
return x;
}
【编译】gcc -Og -S exchange.c
// long exchange(long *xp, long y)
// xp in %rdi, y in %rsi
exchange:
movq (%rdi), %rax
movq %rsi, (%rdi)
ret
【说明】C语言的指针其实对应的就是取内存地址
3.4.4 压入和弹出栈数据
- 栈:
后进先出原则的数据结构。可以看做一个数组,分两个方向:栈顶和栈底- 栈底:高地址
- 栈顶:低地址,属于插入和删除数据的一端。
%rsp保存栈顶元素的地址
- 栈向下增长,栈顶元素的地址一般是最低的
【说明】
- 过程:
- 假设栈已经有一些数据,栈顶是0x108
- 此时执行pushq,则栈顶-8字节,寄存器值填入对应栈的内存
- 此时执行popq,则栈顶+8字节,读取内存栈的内存到寄存器
pushq %rax:将四字节数据压栈。等价于以下两条命令:
subq $8, %rsp //Decrement stack pointer
movq %rax, (%rsp) //Store %rax on stack
popq %rdx:栈弹出四字节数据。等价于以下两条命令:
movq (%rsp), %rdx //Read %rdx from stack
addq $8, %rsp //Increment stack pointer
3.5 算数和逻辑操作
3.5.1 加载有效地址leaq
leaq指令形式:leaq (%rdi, %rsi, 4), %rax
与movq区别:第一个指令看起来是内存形式,但是其实没有读取内存,而是直接将有效地址写到目的操作数
【理解】第一个指令是内存形式的话,相当于利用了 简化了指令运算
【例】书中例子,使用leaq简化函数的算数计算
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 //rax = x+4*y
leaq (%rdx,%rdx,2), %rdx //rdx = z+2*z = 3z
leaq (%rax,%rdx,4), %rax //rax = (x+4*y) + 4*(3z) = x+4*y+12*z
ret
3.5.2 一元和二元操作
【例】书中例子
incq (%rsp):使得栈顶元素+1,即C语言的++运算subq %rax, %rdx:rdx-rax的结果赋值给rdx,即C语言的x-=y
【注】一元和二元操作的目的操作数不能为立即数(理解为立即数不能作为左值,寄存器和内存地址可以)
3.5.3 移位操作
- 左移:算数左移(
SAL)、逻辑左移(SHL),效果一样,都是右边填0 - 右移:算数右移(
SAR,填0)、逻辑右移(SHR,填符号)
【注】移位量可以是立即数,或单字节存放在寄存器%cl中
【例】书中例子,使用leaq和salq代替整数乘法。使用mulq太费时间
leaq (%rdx,%rdx,2), %rax //rax = rdx+rdx*2 = 3*rdx
salq $4, %rax //rax = rax<<4 = 48*rdx
3.5.5 特殊的算数操作
- 乘法运算和除法运算:由于结果溢出,存放到两个寄存器
rax和rdx中:
【例】书中例子
#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
mulq %rdx
movq %rax, (%rdi)
movq %rdx, 8(%rdi)
ret