程序的机器级表示(一)
注:以下内容均来自开源学习组织DataWhale程序的机器级表示(一)1 由程序引入的基本概述程序main.c如下:#include <stdio.h>void mulstore(long x,long y,long *);int main() {long d;multstore(2,3,&d) ;printf(” 2∗ 3 −−>%1d \n”,d);return 0;
注:以下内容均来自开源学习组织DataWhale
程序的机器级表示(一)
1 由程序引入的基本概述
程序main.c如下:
#include <stdio.h>
void mulstore(long x,long y,long *);
int main() {
long d;
multstore(2,3,&d) ;
printf(” 2∗ 3 −−>%1d \n”,d);
return 0;
}
long mult2(long a,long b){
png s = a* b;
return s;
}
程序mstore.c如下
long mult2(long, long);
void mulstore(long x, long y, long *dest){
long t = mult2(x, y);
*dest = t;
}
对mstore.c使用linux> gcc -Og -S mstore.c
以下命令生成汇编文件(mstore.s)如下:
.file "mstore.c"
.text
.globl mulstore
.type mulstore, @function
mulstrore:
.LFBO:
.cfi_startproc
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
...
. 开头的行是指导汇编器和链接器的伪指令,将其忽略掉,只看与C源文件对应部分如下:
mulstrore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
pushq 指令的意思是将寄存器rbx的值压入程序栈进行保存。
popq 指令意思是回复寄存器rbx的内容
2 为什么程序执行前先要pshq保存寄存器rbx的值呢?
2.1 寄存器
在Intel x86-64处理器中包含16个通用目的的寄存器,这些寄存器用来存放整数数据和指针。这些寄存器都是以%r开头
%rax %rbx %rcx %rdx
%rsi %rdi %rbp %rsp
%r8 %r9 %r10 %r11
%12 %r13 %r14 %r15
64位寄存器概览:
最早8086处理器只有8个16位的寄存器(即下图中0-15位),当从16位扩展到32位时,寄存器也扩展到32位(即下图中0-31位),目前寄存器已经扩展到了64位。
例如:变量a是long类型,需占用8个bytes,那么寄存器rax全部用来保存a;如果a是int类型,需占用4个bytes,那么就用eax保存,以此类推。
寄存器角色分配:
2.2 保存器
以函数A、B为例:函数A调用函数B,函数A称为调用者,函数B称为被调用者
A,B的汇编代码如下:
fnnc_A:
...
movq $123, %rbx
call func_B
addq %rbx, %rax
...
ret
fnnc_B:
...
addq %456, %rbx
...
ret
由于A调用了B,寄存器rbx在函数B中被修改,但逻辑上寄存器rbx在调用函数B前后应该保持一致,因此保持rbx不变的两种策略如下:
-
调用者保存(caller saved):函数A在调用函数B之前,提前保存rbx的内容,B被调用执行完后,再恢复rbx的内容。
-
被调用者保存(callee saved):函数B在使用rbx之前先保存rbx的内容,当B返回之前,先恢复rbx的内容。
根据寄存器的不同选择具体使用哪一种保存策略:
-
Callee saved: %rbx, %rbp, %r12, %r13, %r14, %r15
-
Caller saved: %r10, %r11, %rax, %rdi, %rsi, %rdx, %rdx, %r8, %r9
通过上面这段例子就回答了标题2的问题
再看回mulstore.s
mulstrore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
因为rbx是被调用者保存的寄存器,因此 mostore.s中 的pushq
指令就是在 mulstore 函数使用rbx之前保存rbx的内容,popq
就是在函数返回之前,恢复rbx的内容。
3 继续阅读mulstore.s的汇编代码
C代码:
long mult2(long, long);
void mulstore(long x, long y, long *dest){
long t = mult2(x, y);
*dest = t;
}
汇编代码:
mulstrore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
根据寄存器用法定义,函数mulstore三个参数x、y、dest分别保存在寄存器rdi、rsi、rdx中(x → \rightarrow →%rdi,y → \rightarrow →%rsi,dest → \rightarrow →%rdx)。
movq %rdx, %rbx
:是将寄存器rbx的内容复制到rdx,这条指令执行完毕后,寄存器rbx与rdx内容一致,都是dest指针指向的内存地址。这条指令中q表示数据的大小。
call mult2
:指令对应C代码中的mult2函数调用,该函数的返回值即x*y保存在寄存器rax中。
movq %rax, (%rbx):
将寄存器rax的值送到内存中,内存的地址就存放在rbx中。
3.1 汇编码后缀
Intel用字(word)表示16位的数据类型,因此32位数据类型称为双字,64位称为四字。C语言基本类型对应汇编码后缀如下:
C声明 | Intel数据类型 | 汇编码后缀 | 大小(byte) |
---|---|---|---|
char | Byte | b | 1 |
short | Word | w | 2 |
int | Double word | l | 4 |
long | Quad word | q | 8 |
char * | Quad word | q | 8 |
float | Single precision | s | 4 |
double | Double precision | l | 8 |
例如:movb是move byte的缩写,表示传送字节;movw是move word的缩写。以此类推。
4 指令
大多数指令包含两部分:操作码(决定了CPU执行操作的类型)和操作数(有一个或多个)。ret返回指令没有操作数。
操作码 操作数
movq (%rdi), %rax
addq $8, %rsx
subq %rdi, %rax
xorq %rsi, %rdi
ret
4.0 操作数可以分为三类:
- 立即数(Immediate):在 AT&T 格式的汇编中,立即数以 $ 符号开头,后跟一个 C 定义的整数。
- 寄存器(Register):操作数为64位寄存器的情况,向下兼容32、16、8位的寄存器。
- 内存引用(Memory Reference):寄存器带小括号。
4.0.1 内存引用
内存引用用 M b [ a d d r ] M_b[addr] Mb[addr](addr表示有效地址,下标b可以省略)表示,常用的内存引用包含四个部分:
-
立即数(Immediate): I m m Imm Imm
-
基址寄存器(Base Register): r b r_b rb
-
变址寄存器(Index Register): r i r_i ri
-
比例因子(Scale Factor): s s s,取值必须是1、2、4、8。例如char就是1,int就是4,double就是8。
有效地址是通过立即数与基址寄存器的值相加,再加上变址寄存器与比例因子的乘积。如下:
I m m ( r b , r i , s ) → I m m + R [ r b ] + R [ r i ] × s Imm(r_b, r_i, s)\rightarrow Imm+R[r_b]+R[r_i]\times s Imm(rb,ri,s)→Imm+R[rb]+R[ri]×s
内存引用的各种形式,都是以上述公式进行基准的变种,见下表:
4.1 mov指令
mov类指令含有两个操作数,一个为源操作数,另一个为目的操作数。
-
源操作数:可以是一个立即数、一个寄存器或内存引用。
-
目的操作数:用来存放源操作数的内容,所以目的操作数要么是寄存器,要么是内存引用,不能是一个立即数。
例如:
mov memory, register
:将内存的值写入寄存器。
注意:
- x86-64 处理器有一条限制,即mov指令的源操作数和目的操作数不能都是内存地址,所以当需要将一个数从内存一个位置复制到另一个位置时,就需要用两条mov指令。首先将内存源位置数值加载到寄存器,再将寄存器的值写入内存目的位置。
- mov指令的后缀与寄存器的大小一定是匹配的。
- 当movq指令的源操作是立即数时,该立即数只能是32的补码表示,对该数符号位扩展后,将得到64位数传送到目的位置。
- movabsq:该指令的源操作数可以是任意64位立即数,但目的操作数只能时寄存器。
mov指令使用示例(源操作数与目的操作数大小一致的情况):
movabsq $0x0011223344556677, %rax
:将一个64位立即数复制到寄存器rax。寄存器rax内保存的数值如图:
movb $-1, %al
:将-1复制到寄存器al,寄存器al长度为8(见2.1中图),与movb指令后缀一致。此时寄存器rax低8位发生改变:
movw $-1, %ax
:将立即数-1复制到寄存器ax。此时寄存器低16位发生了改变:
movl $-1, %eax
:将立即数-1复制到寄存器eax。此时不仅寄存器低32位发生变化,高32位也发生了变化:
当源操作数的数位小于目的操作数,需要对目的操作数剩余的字节进行零扩展或符号位扩展。
-
零扩展:movzbw、movzbl、movzwl、movzbq、movzwq
以movzbw为例:z表示0,b为源操作数的大小即1byte,w为目的操作数的大小即2bytes。
零扩展没有movlq,因为movlq可以用movl代替。
-
符号位扩展:movsbw、movsbl、movswl、movsbq、movswq、movslq、cltq
以movsbw为例:s为sign的缩写,b、w同上。
cltq
等价于movslq %eax, %rax
5 数据传输
例如有如下C代码:
long exchange(long *xp, long y){
long x = *xp;
*xp = y;
return x;
}
对应汇编代码(其中根据惯例,C中*xp存在寄存器%rdi中,y存在寄存器%rsi中):
exchange:
movq (%rdi), %rax
movq %rsi, (%rdi)
ret
movq (%rdi), %rax
:读取内存中值(*xp指向的内存位置)到寄存器rax(因为返回值是x,所以直接将x放到rax),对应long x = *xp;
。
movq %rsi, (%rdi)
:将rsi中的值写到rsi(*xp),对应*xp = y;
通过这个例子可知:C语言指针即为内存中地址。
6 程序栈
例如保存寄存器rax内数据0x123:
6.1 压栈
可以使用``pushq`指令把数据压入栈内,可分为两步:
-
假设未压栈前指针rsp地址为0x108:
压栈第一步就是寄存器rsp的值减8,此时rsp的值为0x100(对应指令
subq $8, %rsp
)。这里为什么是减呢?因为栈底为高地址,栈顶为低地址,所以压栈地址会降低。
-
第二步就是将保存的数据复制到新的栈顶地址,这时0x100处就保存了rax的值(对应指令
movq %rax, (%rsp)
)。因此
pushq
等效于步骤1、2的两条指令
6.2 弹栈
- 首先从栈顶位置读出数据,复制到寄存器rbx,此时指针rsp的地址还是0x100
-
然后将栈顶指针加8,地址变为0x108。这时0x100地址保存的数据还没被覆盖,下次push操作时才会被覆盖。
popq操作也等效于movq和addq
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)