分析一个简单的汇编代码

分析一个简单的汇编代码

部分常见的寄存器

寄存器16位32位64位
累加寄存器AXEAXRAX
基址寄存器BXEBXRBX
计数寄存器CXECXRCX
数据寄存器DXEDXRDX
堆栈基指针BPEBPRBP
变址寄存器SIESIRSI
堆栈顶指针SPESPRSP
指令寄存器IPEIPRIP

一个x86-64的CPU,包含一组16个存储64位值的「通用目的寄存器」。
这些寄存器用来存储「整数数据」和「指针」。

  • 最初的8086中,有8个16位寄存器,即「ax」到「sp」。
  • 扩展到IA32架构时,这些寄存器也扩展到32位,也即「eax」到「esp」。
  • 扩展到x86-64位后,原来的8个寄存器扩展成64位,即「rax」到「rsp」,然后新增了8个寄存器「r8」到「r15」。

8086:第一代单芯片、16位微处理器之一。
IA32:Intel 32位体系结构(Intel Architecture 32-bit)
Intel64:IA32的64位扩展,也称x86-64

环境信息

gcc -v
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
目标:x86_64-redhat-linux
配置为:../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
线程模型:posix
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)

C语言代码

int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(8, 5);
}

汇编代码

执行gcc -S -fno-asynchronous-unwind-tables test_asm.c就可以得到汇编代码。
使用-fno-asynchronous-unwind-tables选项,是为了禁用cfi指令

关于CFI指令的用处,有一个解释:On some architectures, exception handling must be managed with Call Frame Information directives. These directives are used in the assembly to direct exception handling. These directives are available on Linux on POWER, if, for any reason (portability of the code base, for example), the GCC generated exception handling information is not sufficient.
下述是ATT格式的汇编代码。ATT格式也是GCC、OBJDUMP等工具的默认格式。Microsoft的工具和Intel的文档,汇编代码都是Intel格式的。这两种格式不太相同,比如:movq(ATT格式)、mov(Intel格式)。GCC也可以产生Intel格式的汇编代码,只需要带上参数-masm=intel。

	.file	"test_asm.c"
	.text
	.globl	add_a_and_b
	.type	add_a_and_b, @function
add_a_and_b:
	pushq	%rbp			; (6)
	movq	%rsp, %rbp		; (7)
	movl	%edi, -4(%rbp)	; (8)
	movl	%esi, -8(%rbp)	; (9)
	movl	-8(%rbp), %eax	; (10)
	movl	-4(%rbp), %edx	; (11)
	addl	%edx, %eax		; (12)
	popq	%rbp			; (13)
	ret						; (14)
	.size	add_a_and_b, .-add_a_and_b
	.globl	main
	.type	main, @function
main:
	pushq	%rbp			; (1)
	movq	%rsp, %rbp		; (2)
	movl	$5, %esi		; (3)
	movl	$8, %edi		; (4)
	call	add_a_and_b		; (5)
	popq	%rbp			; (15)
	ret						; (16)
	.size	main, .-main
	.ident	"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
	.section	.note.GNU-stack,"",@progbits

(1) pushq %rbp

rbp寄存器,是ebp寄存器64位扩展。意思是扩展栈指针寄存器,存储栈中最高位数据的内存地址。
rbp寄存器的值,在(1)入栈,在(15)出栈。

这主要是为了把函数中用到的rbp寄存器的内容,恢复到函数调用前的状态。
在进入函数之前,我们无法确定rbp寄存器的值是什么,但是由于函数内部也会使用rbp寄存器,所以就需要暂时把rbp寄存器的值先存到栈里面,函数处理完成之后,再从栈中将值恢复到rbp寄存器。

在函数的入口处,将rbp的值入栈保存,在函数的出口处出栈,这是C语言编译器的规定。
这样做是为了确保函数在调用前后,rbp寄存器的值不会改变。

push和pop指令只有一个操作数,我们不需要指定将值push到哪里,以及将哪里的值pop到寄存器。
是因为,对栈进行读写的内存地址,是由rsp栈指针寄存器管理的。

push入栈和pop出栈指令执行之后,rsp寄存器存储的栈指针的值会自动更新。
因为栈是从高地址位向低地址位生长。
push指令是增加栈元素的操作,所以执行push后,rsp寄存器的值会-4(64位机器就是-8)。
pop指令是减少栈元素的操作,所以执行pop后,rsp寄存器的值会+4(64位机器就是+8)。

我们可以认为,push和pop指令,就是用来在寄存器和栈(主存)之间进行操作的。
push指令就是将寄存器的值,保存到主存中。
pop指令就是将主存中保存的值恢复到寄存器里。

(2) movq %rsp, %rbp

mov指令有这几种:movb(8位)、movw(16位)、movl(32位)、movq(64位)
mov指令的基本格式是:movx source, destination
所以上面(2)的含义是,将rsp寄存器的值,传递到rbp中,这样就形成了main函数的栈帧。

系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。
main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

(3) movl $5, %esi

将数字5,传递到esi寄存器。

(4) movl $8, %edi

将数字8,传递到edi寄存器。

(5) call add_a_and_b

调用add_a_and_b函数。

在将函数的入口地址,设定到程序计数器之前,
call指令会把调用函数结束后,要执行的那一条指令的地址,存储在栈中(也就是主内存中)。

函数执行完毕后,执行ret指令,就会把刚刚说的保存到栈中的地址,设定到程序计数器中。

程序计数器,就是用来存储了下一条指令所在内存的地址。
CPU的控制器,会参照程序计数器的数值,从内存中读取指令,并执行。

(6) pushq %rbp

作用同(1)

(7) movq %rsp, %rbp

作用同(2),为了形成了add_a_and_b函数的栈帧

(8) movl %edi, -4(%rbp)

rbp寄存器,在步骤(7)已经被更新为rsp寄存器的值了,也就是当前add_a_and_b函数的栈帧首地址。
将edi寄存器,此时保存的值是8,传送到rbp-4的位置,也就是第一个参数入栈了

(9) movl %esi, -8(%rbp)

将esi寄存器,此时保存的值是5,传送到rbp-8的位置,也就是第二个参数入栈了

(10) movl -8(%rbp), %eax

将rbp-8地址的值,也就是5,传送到eax寄存器。
eax,累加寄存器,主要用来做加法运算。

(11) movl -4(%rbp), %edx

将rbp-4地址的值,也就是8,传送到edx寄存器。

(12) addl %edx, %eax

加法指令格式:ADD A,B //A=A+B;
将edx与eax中的数值相加,结果存在edx中

(13) popq %rbp

取出栈中最近一次写入的值并写入到rbp寄存器,其实就是步骤(6)存入栈的值。
pop指令还会将esp寄存器的地址加4,回收栈帧。
64位寄存器就是将rsp寄存器的地址加8,回收栈帧。

(14) ret

ret指令的作用,在步骤(5)中已涉及

(15) popq %rbp

上述已提到

(16) ret

ret指令的作用,在步骤(5)中已涉及

参考链接

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值