先通过Xcode创建一个terminal APP,语言选择C。代码如下:
#include <stdio.h>int main(int argc, const char * argv[]) {int a[7]={1,2,3,4,5,6,7};int *ptr =(int*)(&a+1);printf("%d\n",*(ptr));return 0;
}
在return 0处打上断点,并且选择Xcode菜单Debug|Debug Workflow|Always Show Disassembly,然后run。这时候断点会调到汇编代码里。得到汇编代码如下:
Terminal`main:0x100003ec0 <+0>: pushq %rbp0x100003ec1 <+1>: movq %rsp, %rbp0x100003ec4 <+4>: subq $0x50, %rsp0x100003ec8 <+8>: movq 0x131(%rip), %rax ; (void *)0x00007ff84ef2f8a0: __stack_chk_guard0x100003ecf <+15>: movq (%rax), %rax0x100003ed2 <+18>: movq %rax, -0x8(%rbp)0x100003ed6 <+22>: movl $0x0, -0x34(%rbp)0x100003edd <+29>: movl %edi, -0x38(%rbp)0x100003ee0 <+32>: movq %rsi, -0x40(%rbp)0x100003ee4 <+36>: movq 0xa5(%rip), %rax0x100003eeb <+43>: movq %rax, -0x30(%rbp)0x100003eef <+47>: movq 0xa2(%rip), %rax0x100003ef6 <+54>: movq %rax, -0x28(%rbp)0x100003efa <+58>: movq 0x9f(%rip), %rax0x100003f01 <+65>: movq %rax, -0x20(%rbp)0x100003f05 <+69>: movl 0x9d(%rip), %eax0x100003f0b <+75>: movl %eax, -0x18(%rbp)0x100003f0e <+78>: leaq -0x30(%rbp), %rax0x100003f12 <+82>: addq $0x1c, %rax0x100003f16 <+86>: movq %rax, -0x48(%rbp)0x100003f1a <+90>: movq -0x48(%rbp), %rax0x100003f1e <+94>: movl (%rax), %esi0x100003f20 <+96>: leaq 0x85(%rip), %rdi ; "%d\n"0x100003f27 <+103>: movb $0x0, %al0x100003f29 <+105>: callq 0x100003f5a ; symbol stub for: printf0x100003f2e <+110>: movq 0xcb(%rip), %rax ; (void *)0x00007ff84ef2f8a0: __stack_chk_guard0x100003f35 <+117>: movq (%rax), %rax0x100003f38 <+120>: movq -0x8(%rbp), %rcx0x100003f3c <+124>: cmpq %rcx, %rax0x100003f3f <+127>: jne 0x100003f4d ; <+141> at main.c
-> 0x100003f45 <+133>: xorl %eax, %eax0x100003f47 <+135>: addq $0x50, %rsp0x100003f4b <+139>: popq %rbp0x100003f4c <+140>: retq 0x100003f4d <+141>: callq 0x100003f54 ; symbol stub for: __stack_chk_fail0x100003f52 <+146>: ud2
首先介绍下面会用到的几个寄存器:
rip :程序计数寄存器
rsp : 栈指针寄存器,指向栈顶
rbp : 栈基址寄存器,指向栈底
edi : 函数参数
rsi/esi : 函数参数
eax : 累加器或函数返回值用
1、 pushq %rbp
将rbp的地址压栈,rsp继续指向栈顶
2、 movq %rsp, %rbp
将栈顶rsp的值赋值给栈底rbp,
3、 subq $0x50, %rsp
栈顶往下移5*16个字节,可以理解成给后面预留的80字节的空间。X64中栈开辟的大小都是0x10的倍数。
4、movq 0x131(%rip), %rax ; (void *)0x00007ff84ef2f8a0: __stack_chk_guard
0x131(%rip)的意思是下一条指令的地址(0x100003ecf)加上0x131得到目标地址(0x100004000),然后取得其中的8字节值,设置给rax寄存器。
选中Debug workflow|View Memory,在Address里输入‘0x100004000’然后回车,我们可以看到此处的内容为0x00007ff84ef2f8a0(注意大小端问题):
5、movq (%rax), %rax
从rax寄存器存放的地址处取得值,并传给rax寄存器。类似前面的操作,我们发现此处的值为0x55d1d55afee700d6:
6、movq %rax, -0x8(%rbp)
现在把rax中的值,也就是上面这个图上所示的8个字节,存入rbp-0x8的位置。我们先打印下rbp和rsp的值,然后跳到rsp处查看内存:
红框处即是我们存放值的地方。右边8字节则是rbp指向的位置。
7、movl $0x0, -0x34(%rbp)
将4字节0设置到rbp-0x34的位置,这里的目的是将下一条指令中-0x38(%rbp)的高字节清零。该位置为0x7ff7bfeff3ac:
8、movl %edi, -0x38(%rbp)
该命令保存edi寄存器的值到rbp-0x38位置,也就是上图的0x7ff7bfeff3a8,值是1。前面我们说edi是用来保存函数参数的,也就是int argc,在这个例子中argc的值为1,所以edi寄存器的值为1。
9、movq %rsi, -0x40(%rbp)
该命令保存rsi寄存器的值到rbp-0x40位置,也就是上图的0x7ff7bfeff3a0,值是0x7ff7bfeff518。这里是参数argv的值0x7ff7bfeff708。由于argv是const char **,因此这也是个地址值,我们前往该地址查看其内容。
10、movq 0xa5(%rip), %rax
获取位于0x100003eeb+0xa5=0x100003f90处的8字节内容,然后存入rax寄存器:
11、movq %rax, -0x30(%rbp)
将rax寄存器的值存入rbp-0x30的位置:
12-17跟上面两步相同,就是存3,4,5,6,7的值,注意7是单独存的,因为movl表示4字节,而movq代表8字节,也就是2个int。
0x100003eef <+47>: movq 0xa2(%rip), %rax0x100003ef6 <+54>: movq %rax, -0x28(%rbp)0x100003efa <+58>: movq 0x9f(%rip), %rax0x100003f01 <+65>: movq %rax, -0x20(%rbp)0x100003f05 <+69>: movl 0x9d(%rip), %eax0x100003f0b <+75>: movl %eax, -0x18(%rbp)
18、leaq -0x30(%rbp), %rax
获取rbp-0x30=0x7ff7bfeff3b0,然后存入rax寄存器。
19、addq $0x1c, %rax
将rax寄存器中的值0x7ff7bfeff3b0,加上0x1c后(0x7ff7bfeff3cc)存入rax寄存器。这里对应代码`int *ptr =(int*)(&a+1);`0x1c是28,也就是数组的大小28字节,说明指针的加法是在编译阶段将数值替换为具体的值,也就是该值乘以指针类型大小后的值。
20、movq %rax, -0x48(%rbp)
接下去把rax的值0x7ff7bfeff3cc存入rbp-0x48的位置:
21、movq -0x48(%rbp), %rax
接着又把刚存入的这个值存入寄存器rax
22、movl (%rax), %esi
把rax寄存器所存地址对应的值(1)存入寄存器esi,这是作为下面要调用的print方法的第二个参数。
23、leaq 0x85(%rip), %rdi ; "%d\n"
rip=0x100003f27,加上0x85后为0x100003fac,然后设置给rdi寄存器,也就是print调用的第一个参数。0x100003fac这个位置的值刚好是字符串"%d\n"。
24、movb $0x0, %al
将立即数0存储到al寄存器中。那么如何理解eax,ax,al(ah)之间的关系
专业点可以这样解释:eax是32位寄存器,ax是16位寄存器,al(ah)是八位寄存器。
对于变长参数的函数,要用%al指明用到的vector registers的个数 ,比如printf,这里我们没有用到可变参数,所以要给al寄存器设置0。参考:Why are the %al register and stack modified before calling printf x86 assembly from C "Hello World" program compiled by gcc
汇编函数调用的参数传递
25、callq 0x100003f5a ; symbol stub for: printf
调用printf函数。call有一个作用:将call指令的下一条指令地址压栈:
26、movq 0xcb(%rip), %rax ; (void *)0x00007ff84ef2f8a0: __stack_chk_guard
27、movq (%rax), %rax
26和27步骤跟4和5处类似,这里不再赘述。
28、movq -0x8(%rbp), %rcx
将栈底8字节存入rcx寄存器
29、cmpq %rcx, %rax
比较rcx寄存器和rax寄存器的值是否相等,并把结果写入状态寄存器
30、jne 0x100003f4d ; <+141> at main.c
如果29的比较结果为不相等,就跳转0x100003f4d处继续执行,也就是35处。相等的话执行31处。这里主要是使用__stack_chk_guard_ptr来判断是否发生栈溢出,导致栈底开始8字节被篡改。可以参考關於__stack_chk_guard_ptr的理解
31、xorl %eax, %eax
将eax寄存器清零,作为main函数的return值
32、addq $0x50, %rsp
这句正好对应前面的subq $0x50, %rsp。通过给栈顶指针加上开辟栈的大小,回收栈顶指针开辟的空间。
33、popq %rbp
这句指令表示出栈,同时将出栈的值放入寄存器rbp
34、retq
这句表示退出main函数,会恢复rip值。本例没有体现这一点,由main函数的调用者保存rip。
35、callq 0x100003f54 ; symbol stub for: __stack_chk_fail
调用__stack_chk_fail函数
36、ud2
UD2 指令的字节编码为 0F 0B,它是一个两字节的指令。在汇编语言中,可以使用 UD2 指令来实现一些特殊的功能,比如触发调试断点或者中断程序的执行。 UD2 指令常用于调试程序。具体可查看:ud2 汇编指令
最终的栈布局