深入理解汇编语言和c++语言(1)---从a+b说起

最近突然对c语言,以及c语言如何转换到汇编语言特别的感兴趣,于是写了这个系列,首先让我们看一个小程序:

using namespace std;

int main(){
int s;
int a=3;
int b=4;
s=a+b;
return 0;
}

这是个很简单c++程序,仅仅把两个局部变量相加而已,我们汇编这段代码:

g++  -S add.cc  -o add.s

以上命令就是把add.cc这个c++源文件汇编成了add.s这个汇编源文件.这个时候我们查看add.s这个文件,里面有一段汇编程序如下:

pushq	%rbp
movq	%rsp,        %rbp
movl	$3,         -12(%rbp)
movl	$4,         -8(%rbp)
movl	-8(%rbp),   %eax
movl	-12(%rbp),  %edx
addl	%edx,       %eax
movl	%eax,       -4(%rbp)
movl	$0,         %eax
popq	%rbp
ret

首先我们看我们感兴趣两个汇编指令:

movl	$3, -12(%rbp)     ##a=3
movl	$4, -8(%rbp)      ##b=4
这两句完成了局部变量的赋值操作,这个rbp为base pointer基址指针寄存器,保存了该函数栈空间的一个快照sanshot. -12(%rbp)为栈空间向下12个字节位置的内存地址, movl为移动一个long word(32 bytes)的数值.因此,从rbp-12,rbp-11,rbp-10,rbp-9这4个byte将被赋值为32位的3.注意在int 为32为字长的整形操作数.

addl	%edx, %eax     ## s = a+b

这个汇编指令比较直观,仅仅是把eas寄存器操作数和edx寄存器操作数相加,存放到eas寄存器操作数中,但是刚刚我们说过,程序的局部变量是存储在栈空间里的,那是如何跑到寄存器里的呢?

movl	-8(%rbp),   %eax
movl	-12(%rbp),  %edx

这两个指令干的活就是从栈空间把局部变量load到寄存器里,参与运算,当然也许你会觉得这不是多次一举嘛,但是为什么不直接执行类似addi的指令呢,但是设想如果你有几十个局部变量,请问你如何管理这些变量(注意这些参数的管理都是编译器帮你完成的)
movl	%eax,  -4(%rbp)

这个指令仅仅是把计算出来的结果也就是s的值依然保存在栈里面,这里我发现一个有趣的现象,先定义的局部变量a分配在了距离栈顶比较近的位置(rbp-12)(why? 因为栈的增长方向总是向低地址空间增长的).而后定义的反而比较靠后(s 存在了rbp-4). 我觉得也许是因为编译器在做语法分析的时候是以bottom up 的方式进行的(有兴趣的朋友可以研究一下).

pushq	%rbp
movq	%rsp, %rbp

这个我们称之为函数的开场白,基本上所有的函数调用开始都为这两句,其实它们的功能很简单,保存上一个函数的现场,也就是rbp入栈,为新进入的函数建立新的现场也就是rsp赋值给rbp,其实我们所有在栈上的操作都是rbp和rsp交替更新完成的.设想如果存在void a()--->void b()--->void c()这样的连续调用,每个函数都有自己的栈空间,但是整个程序的栈空间是统一的,所以在每次进入到一个新的函数时第一件事就是保存上一个函数的栈现场(pushq  %rbp)然后更新到本函数的栈现场(movq  %rsp  %rbp)

movl	$0, %eax
popq	%rbp
ret
把0放到eax中主要用于设置函数的返回值为0,pop操作用于恢复之前保存的栈现场(push %rbp),然后返回上一层函数.到此为止整个函数就分析完毕了,其实除去保存现场和恢复现场这样常规的操作整个汇编代码很容易理解.

注意:

1. 我是在gnu工具链(其实g++ -S 等同于as)汇编的的代码,所以这是AT&T的汇编格式,有别于intel的汇编代码格式

2. 操作系统的64位的,所以寄存器rbp相当于sbp的64位扩展


展开阅读全文

没有更多推荐了,返回首页