CSAPP–第三章-程序的机器级表示(上)

Aki 发布于 2022-10-25 208 次阅读


学习参考文章,视频:

快速入门汇编语言 - 知乎 (zhihu.com)

【CSAPP-深入理解计算机系统】3-1.程序的机器级表示_哔哩哔哩_bilibili

程序的机器级表示_晨哥是个好演员的博客-CSDN博客_程序的机器级表示

【CSAPP】第三章 程序的机器级表示 - 码农教程 (manongjc.com)

程序编码、

gcc编译过程:

  • 预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释#删掉,这里并不会检查语法;
  • 编译:检查语法,将预处理后的文件编译成汇编文件;
  • 汇编: 将汇编文件生成目标文件(二进制文件);
  • 链接: C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到可执行程序中去。

命令:

步骤命令
预处理gcc -E hello.c -o hello.i
编译gcc -S hello.i -o hello.s
汇编gcc -c hello.s -o hello.o
链接gcc hello.o -o hello_elf
  • 如果要一步到位直接生成可执行文件,命令为gcc hello.c -o hello
  • 可以在gcc后指定 -Og/-O1/-O2设定编译优化级别,-Og 为原生样式,与源代码最相近,使用优化可以提高性能,不过优化后代码会和原先的代码有不同。

现在有两个文件,一个为 mstroe.c,另一个为main.c

//mstore.c
double mult2(double, double);
void multstore(double x, double y, double *dest)
{
	double t = mult2(x,y);
	*dest = t;
}
//main.c
#include<stdio.h>
#include<stdlib.h>
#include<mstore.c>
void multstore(double, double, double*);
int main()
{
	double d;
	multstore(2, 3, &d);
	printf("2 * 3-->%ld\n", d);
	system("pause");
	return 0;
}
double mult2(double a, double b){
	double s = a * b;
	return s;
}

将 mstore.c 汇编后,gcc mstore.c -S -Og -o mstore.s,得到一个汇编文件,打开看是下面这样的

          .file   "mstore.c"
          .text
          .globl  multstore
          .type   multstore, @function
multstore:
.LFB0:
         .cfi_startproc
         pushq   %rbx
         .cfi_def_cfa_offset 16
         .cfi_offset 3, -16
         movq    %rdi, %rbx
         call    mult2
         movsd   %xmm0, (%rbx)
         popq    %rbx
         .cfi_def_cfa_offset 8
         ret
         .cfi_endproc
.LFE0:
         .size   multstore, .-multstore
         .ident  "GCC: (GNU) 8.5.0 20210514 (Red Hat 8.5.0-15)
         .section        .note.GNU-stack,"",@progbits

其中以 . 开头的行都是指导汇编器和链接器工作的伪指令,也就是说我们完全可以忽略这些以 . 开头的行,删除掉无关信息后,剩余的这些汇编代码就是与源文件的c代码有关的代码。

multstore:
 pushq   %rbx
 movq    %rdi, %rbx
 call    mult2
 movsd   %xmm0, (%rbx)
 popq    %rbx
 ret

简单介绍一下寄存器的知识:

在牙膏厂x86_64位的处理器中包含有16个通用目的的寄存器,这些寄存器用来存放整数数据和指针,且都是以%r开头。

介绍两个概念 -- 调用者保存寄存器和被调用者保存寄存器:

假设有如下场景:函数A调用了函数B,函数A称为调用者,函数B称为被调用者。由于调用了函数B,寄存器rbx的值被修改了,但是逻辑上寄存器rbx的值需要在调用B函数前后保持一致。解决者问题有下面两个方法。

func A:
	...
	call func B
	...

func B:
	...

调 用 者 保 存 寄 存 器 :

func A:
	...
	save register %rbx       在调用B函数之前保存寄存器rbx的值
	call func B
	restore register %rbx    调用结束后回复寄存器rbx的值
	...

func B:
	...

如上述代码所示,寄存器%rbx是由函数B的调用者,即函数func A来保存并且恢复的。函数B感知不到这个情况,可以尽情使用寄存器%rbx。

被 调 用 者 保 存 寄 存 器 :

func A:
	...
	call func B
	...

func B:
	save register %rbx         函数B在使用寄存器rbx之前先保存寄存器rbx的值
	...
	restore register %rbx      使用完后回复寄存器rbx的值

如代码所示,寄存器%rbx是由被调用函数,即函数B来保存并回复的。函数A感知不到这个情况。

对于这16个寄存器要使用哪一种策略,有具体的定义,如下所示:

  • 调用者保存: %rax,%rdi,%rsi,%rdx,%rcx,%8,%r9,%r10,%r11
  • 被调用者保存: %rbx ,%rbp,%r12,%r13,%r14,%r15

接下来我们先去了解几个主要寄存器的功能:

可以看到rax一般用于存放要返回的值,rdirsirdxrcxr8r9分别用于存放当前函数的六个参数值,这七个寄存器均采用调用者保存寄存器策略,rsp寄存器在上面提到过,是栈顶指针寄存器。

了解了这些后,这样就可以解释一些汇编代码了:

multstore:
 pushq   %rbx
 movq    %rdi, %rbx
 call    mult2
 movsd   %xmm0, (%rbx)
 popq    %rbx
 ret


double mult2(double, double);
void multstore(double x, double y, double *dest)  
{
	double t = mult2(x,y);
	*dest = t;
}

寄存器%xmm0 和 movsd 都是新的玩意,我是在linux云服务器编译的,与原书本上的不同,但是功能是相同的。

pushq %rbx 的意思为保存寄存器 rbx 的内容,而 popq %rbx 的意思为恢复寄存器 rbx 的内容。

movq %rdi,%rbx 的意思为将寄存器 rdi 的值放到寄存器 rbx 上。而 mov 后面的 q 表示的是数据的大小,如下所示:

继续分析我们的代码,根据上面不同寄存器具有不同功能得知,mulstore函数的三个参数分别保存至rdirsirdx中:

call mult2 则是调用mult2函数,将得到的结果保存在寄存器 %xmm0 中。

movsd %xmm0, (%rbx) 则是将寄存器 %xmm0 的结果发送到内存中保存,可以看到rbx寄存器外加了括号,这类似C语言中指针解引用,说明rbx存放的是地址,我们通过地址寻找到具体地址中存放的值,即代码中dest指针指向的值,然后将 xmm0 寄存器的值移动至rbx中,xmm0 存放着mult2函数的返回值。

ret 则是函数返回,退出。