注:以下内容均来自开源学习组织DataWhale

程序的机器级表示(一)

1 由程序引入的基本概述

程序main.c如下:

#include <stdio.h>
void mulstore(long x,long y,long *);
int main() {
	long d;
    multstore(23&d) ;
	printf(23 −−>%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)
charByteb1
shortWordw2
intDouble wordl4
longQuad wordq8
char *Quad wordq8
floatSingle precisions4
doubleDouble precisionl8

例如: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:将内存的值写入寄存器。

注意:

  1. x86-64 处理器有一条限制,即mov指令的源操作数和目的操作数不能都是内存地址,所以当需要将一个数从内存一个位置复制到另一个位置时,就需要用两条mov指令。首先将内存源位置数值加载到寄存器,再将寄存器的值写入内存目的位置。
  2. mov指令的后缀与寄存器的大小一定是匹配的。
  3. 当movq指令的源操作是立即数时,该立即数只能是32的补码表示,对该数符号位扩展后,将得到64位数传送到目的位置。
  4. movabsq:该指令的源操作数可以是任意64位立即数,但目的操作数只能时寄存器。

mov指令使用示例(源操作数与目的操作数大小一致的情况):

  1. movabsq $0x0011223344556677, %rax:将一个64位立即数复制到寄存器rax。寄存器rax内保存的数值如图:

在这里插入图片描述

  1. movb $-1, %al:将-1复制到寄存器al,寄存器al长度为8(见2.1中图),与movb指令后缀一致。此时寄存器rax低8位发生改变:

在这里插入图片描述

  1. movw $-1, %ax:将立即数-1复制到寄存器ax。此时寄存器低16位发生了改变:

在这里插入图片描述

  1. 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`指令把数据压入栈内,可分为两步:

  1. 假设未压栈前指针rsp地址为0x108:在这里插入图片描述

    压栈第一步就是寄存器rsp的值减8,此时rsp的值为0x100(对应指令subq $8, %rsp)。

    这里为什么是减呢?因为栈底为高地址,栈顶为低地址,所以压栈地址会降低。

  2. 第二步就是将保存的数据复制到新的栈顶地址,这时0x100处就保存了rax的值(对应指令movq %rax, (%rsp))。

    因此pushq等效于步骤1、2的两条指令

6.2 弹栈

  1. 首先从栈顶位置读出数据,复制到寄存器rbx,此时指针rsp的地址还是0x100

在这里插入图片描述

  1. 然后将栈顶指针加8,地址变为0x108。这时0x100地址保存的数据还没被覆盖,下次push操作时才会被覆盖

    popq操作也等效于movq和addq

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐