C语言代码的x86-64汇编指令分析过程记录

先通过Xcode创建一个terminal APP,语言选择C。代码如下:

cpp 复制代码
#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,点击运行。这时候断点会跳到汇编代码里,得到汇编代码如下:

bash 复制代码
Terminal`main:
    0x100003ec0 <+0>:   pushq  %rbp
    0x100003ec1 <+1>:   movq   %rsp, %rbp
    0x100003ec4 <+4>:   subq   $0x50, %rsp
    0x100003ec8 <+8>:   movq   0x131(%rip), %rax         ; (void *)0x00007ff84ef2f8a0: __stack_chk_guard
    0x100003ecf <+15>:  movq   (%rax), %rax
    0x100003ed2 <+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), %rax
    0x100003eeb <+43>:  movq   %rax, -0x30(%rbp)
    0x100003eef <+47>:  movq   0xa2(%rip), %rax
    0x100003ef6 <+54>:  movq   %rax, -0x28(%rbp)
    0x100003efa <+58>:  movq   0x9f(%rip), %rax
    0x100003f01 <+65>:  movq   %rax, -0x20(%rbp)
    0x100003f05 <+69>:  movl   0x9d(%rip), %eax
    0x100003f0b <+75>:  movl   %eax, -0x18(%rbp)
    0x100003f0e <+78>:  leaq   -0x30(%rbp), %rax
    0x100003f12 <+82>:  addq   $0x1c, %rax
    0x100003f16 <+86>:  movq   %rax, -0x48(%rbp)
    0x100003f1a <+90>:  movq   -0x48(%rbp), %rax
    0x100003f1e <+94>:  movl   (%rax), %esi
    0x100003f20 <+96>:  leaq   0x85(%rip), %rdi          ; "%d\n"
    0x100003f27 <+103>: movb   $0x0, %al
    0x100003f29 <+105>: callq  0x100003f5a               ; symbol stub for: printf
    0x100003f2e <+110>: movq   0xcb(%rip), %rax          ; (void *)0x00007ff84ef2f8a0: __stack_chk_guard
    0x100003f35 <+117>: movq   (%rax), %rax
    0x100003f38 <+120>: movq   -0x8(%rbp), %rcx
    0x100003f3c <+124>: cmpq   %rcx, %rax
    0x100003f3f <+127>: jne    0x100003f4d               ; <+141> at main.c
->  0x100003f45 <+133>: xorl   %eax, %eax
    0x100003f47 <+135>: addq   $0x50, %rsp
    0x100003f4b <+139>: popq   %rbp
    0x100003f4c <+140>: retq   
    0x100003f4d <+141>: callq  0x100003f54               ; symbol stub for: __stack_chk_fail
    0x100003f52 <+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。

bash 复制代码
0x100003eef <+47>:  movq   0xa2(%rip), %rax
    0x100003ef6 <+54>:  movq   %rax, -0x28(%rbp)
    0x100003efa <+58>:  movq   0x9f(%rip), %rax
    0x100003f01 <+65>:  movq   %rax, -0x20(%rbp)
    0x100003f05 <+69>:  movl   0x9d(%rip), %eax
    0x100003f0b <+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 汇编指令

最终的栈布局

相关推荐
雪碧透心凉_2 天前
8086汇编(16位汇编)学习笔记00.DEBUG命令使用解析及范例大全
汇编
C66668885 天前
C#多线程
开发语言·汇编·c#
傻童:CPU5 天前
汇编源程序的理解
汇编
木槿715 天前
软件包git没有可安装候选
汇编·git
ok0606 天前
各种开源汇编、反汇编引擎的非专业比较
汇编·开源
roboko_6 天前
MIPS指令集(一)基本操作
汇编
Crossoads6 天前
【汇编语言】内中断(三) —— 中断探险:从do0到特殊响应的奇妙旅程
android·开发语言·javascript·网络·汇编·单片机·机器学习
染指11107 天前
49.第二阶段x86游戏实战2-鼠标点击call深追二叉树
汇编·c++·windows·游戏安全·反游戏外挂·游戏逆向
程序leo源9 天前
深入理解指针
android·c语言·开发语言·汇编·c++·青少年编程·c#
skywalk816310 天前
好玩的汇编编译器NASM:一款基于x86架构的汇编与反汇编软件
汇编