注:本文内容不涉及浮点数相关内容
不同处理器架构有各自特性,但是在C语言通过编译器生成可执行文件后其背后的运行逻辑基本一致:
理解上述流程,对相关处理器汇编有所了解,即可开启底层的程序行为分析
在学习c语言过程中,学习函数章节时我们就需要认清形参与实参的区别,当我们用熟了C语言后我们就知道,在函数中形参仅仅起到了参数传递的作用,修改形参根本就不会影响到实参的值。
那么为什么是这样的呢?我们先从表面现象开看,修改形参的值不会反过来影响实参的值,这个现象其实表明了形参和实参在计算机中是两个不同的实体,那么我们再进一步分析,在刚进入被调用的函数未对形参做任何修改时形参和实参的值是相同的,因此我们还可以推测出,在函数调用过程中计算机将实参的值复制了一份,从而有了形参。
问题来了,将实参复制后的形参保存在什么地方呢,这里给出结论:当函数的入参个数小于等于一个临界值时,形参放在寄存器里面,当函数入参个数大于这个临界值时,前几个形参放在寄存器里面,后续的形参放在栈内存里面。
在被调用的函数执行完后,函数的返回值其实也放在某个寄存器中,方便被调用者快速获取返回值。
接下来有几个问题以及答案帮助读者理解这部分内容:
接下来再说明两个概念:
所以这就是不同处理器架构里面函数调用 ABI 规范性的由来,想要了解某种特定架构的 ABI 都可以上网查资料或者问问AI大模型找到的。
从c语言的视角来看某个函数的构成时,C语言内部带有局部变量,完成加减乘除算数运算以及各种逻辑运算,完成代码执行流程控制,可以继续调用其他函数。
到了汇编层面后,这些内容相较c语言更加晦涩难懂一些,初学的人往往单条指令能看懂,但是逻辑无法串联起来,c语言在顶层屏蔽了机器的一些细节上的操作,整体粗略的来看,汇编指令其实也不过在做下面这三种流程。
x86_64 架构下各寄存器的名称
通用寄存器
64位寄存器名称 低32位寄存器名称 低16位寄存器名称 低8位寄存器名称 8~15位寄存器名称
rax eax ax al ah
rbx ebx bx bl bh
rcx ecx cx cl ch
rdx edx dx dl dh
rsi esi si sil --
rdi edi di dil --
rbp ebp bp bpl --
r8 r8d r8w r8b --
r9 r9d r9w r9b --
r10 r10d r10w r10b --
r11 r11d r11w r11b --
r12 r12d r12w r12b --
r13 r13d r13w r13b --
r14 r14d r14w r14b --
r15 r15d r15w r16b --
特殊寄存器
程序计数器:rip
栈顶指针: rsp
标志寄存器: rflags
这里的寄存器并没有完整包含所有寄存器,仅挑重要的寄存器来介绍
在x86_64架构下 rbp 可以被用作栈帧基址寄存器,但是在实际编译程序时这个寄存器一般不用作此用途,直接用来当通用寄存器使用。
#include <stdio.h>
int func(int a, short b, char c, long d, long long e, void *f)
{
int ret = 0;
ret = printf("a: %d, b: %hd, c: %c, d: %ld, e: %lld, f: %p\n",
a, b, c, d, e, f);
return ret;
}
int main()
{
int a = 1; // 4 bytes
short b = 2; // 2 bytes
char c = 'r'; // 1 bytes
long d = 3; // 8 bytes
long long e = 4; // 8 bytes
void *f = &c; // 8 bytes
int ret = func(a, b, c, d, e, f);
return ret;
}
为了防止编译器优化,同时编译出来的可执行文件需要有符号表,使用gcc -O0 -g ./main.c ,将上述文件编译,之后使用gdb a.out 命令开启调试,首先看看函数的反汇编长什么样。
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000000011d5 <+0>: endbr64
0x00000000000011d9 <+4>: push %rbp
0x00000000000011da <+5>: mov %rsp,%rbp
0x00000000000011dd <+8>: sub $0x30,%rsp
0x00000000000011e1 <+12>: mov %fs:0x28,%rax
0x00000000000011ea <+21>: mov %rax,-0x8(%rbp)
0x00000000000011ee <+25>: xor %eax,%eax
0x00000000000011f0 <+27>: movl $0x1,-0x28(%rbp)
0x00000000000011f7 <+34>: movw $0x2,-0x2a(%rbp)
0x00000000000011fd <+40>: movb $0x72,-0x2b(%rbp)
0x0000000000001201 <+44>: movq $0x3,-0x20(%rbp)
0x0000000000001209 <+52>: movq $0x4,-0x18(%rbp)
0x0000000000001211 <+60>: lea -0x2b(%rbp),%rax
0x0000000000001215 <+64>: mov %rax,-0x10(%rbp)
0x0000000000001219 <+68>: movzbl -0x2b(%rbp),%eax
0x000000000000121d <+72>: movsbl %al,%edx
0x0000000000001220 <+75>: movswl -0x2a(%rbp),%esi
0x0000000000001224 <+79>: mov -0x10(%rbp),%r8
0x0000000000001228 <+83>: mov -0x18(%rbp),%rdi
0x000000000000122c <+87>: mov -0x20(%rbp),%rcx
0x0000000000001230 <+91>: mov -0x28(%rbp),%eax
0x0000000000001233 <+94>: mov %r8,%r9
0x0000000000001236 <+97>: mov %rdi,%r8
0x0000000000001239 <+100>: mov %eax,%edi
0x000000000000123b <+102>: callq 0x1169 <func>
0x0000000000001240 <+107>: mov %eax,-0x24(%rbp)
0x0000000000001243 <+110>: mov -0x24(%rbp),%eax
0x0000000000001246 <+113>: mov -0x8(%rbp),%rdx
--Type <RET> for more, q to quit, c to continue without paging--c
0x000000000000124a <+117>: xor %fs:0x28,%rdx
0x0000000000001253 <+126>: je 0x125a <main+133>
0x0000000000001255 <+128>: callq 0x1060 <__stack_chk_fail@plt>
0x000000000000125a <+133>: leaveq
0x000000000000125b <+134>: retq
End of assembler dump.
(gdb) disassemble func
Dump of assembler code for function func:
0x0000000000001169 <+0>: endbr64
0x000000000000116d <+4>: push %rbp
0x000000000000116e <+5>: mov %rsp,%rbp
0x0000000000001171 <+8>: sub $0x40,%rsp
0x0000000000001175 <+12>: mov %edi,-0x14(%rbp)
0x0000000000001178 <+15>: mov %esi,%eax
0x000000000000117a <+17>: mov %rcx,-0x28(%rbp)
0x000000000000117e <+21>: mov %r8,-0x30(%rbp)
0x0000000000001182 <+25>: mov %r9,-0x38(%rbp)
0x0000000000001186 <+29>: mov %ax,-0x18(%rbp)
0x000000000000118a <+33>: mov %edx,%eax
0x000000000000118c <+35>: mov %al,-0x1c(%rbp)
0x000000000000118f <+38>: movl $0x0,-0x4(%rbp)
0x0000000000001196 <+45>: movsbl -0x1c(%rbp),%ecx
0x000000000000119a <+49>: movswl -0x18(%rbp),%edx
0x000000000000119e <+53>: mov -0x30(%rbp),%rdi
0x00000000000011a2 <+57>: mov -0x28(%rbp),%rsi
0x00000000000011a6 <+61>: mov -0x14(%rbp),%eax
0x00000000000011a9 <+64>: sub $0x8,%rsp
0x00000000000011ad <+68>: pushq -0x38(%rbp)
0x00000000000011b0 <+71>: mov %rdi,%r9
0x00000000000011b3 <+74>: mov %rsi,%r8
0x00000000000011b6 <+77>: mov %eax,%esi
0x00000000000011b8 <+79>: lea 0xe49(%rip),%rdi # 0x2008
0x00000000000011bf <+86>: mov $0x0,%eax
0x00000000000011c4 <+91>: callq 0x1070 <printf@plt>
0x00000000000011c9 <+96>: add $0x10,%rsp
0x00000000000011cd <+100>: mov %eax,-0x4(%rbp)
--Type <RET> for more, q to quit, c to continue without paging--c
0x00000000000011d0 <+103>: mov -0x4(%rbp),%eax
0x00000000000011d3 <+106>: leaveq
0x00000000000011d4 <+107>: retq
End of assembler dump.
先比较一下两个函数的开头,可以发现两个函数开头的八个字节都是一样的,都是三个相同的指令,endbr64 这个指令标识这是一个函数起始的位置,这个指令和本文主题不大,不做过多赘述。后面第二个指令与第三个指令值得我们关注一下,由于编译的时候设置了O0的优化等级,这就导致了rbp寄存器在处理器运行过程中充当了栈帧基址指针的作用,push %rbp,这条指令将上一个栈帧的基址压入栈,执行完这一步后需要构建当前栈帧,mov %rsp,%rbp 这条指令就设置了当前栈帧基址,继续看后续的第四条指令,这两条指令都是对rsp做了减操作,栈变得更深,腾出了局部变量所需的空间。
为栈帧上的元素腾出空间:在main函数中函数开始的地方通过 sub $0x30,%rsp 命令腾出了48字节的栈空间空间,用于存放该栈帧里面一些局部的值,包括但不限于局部变量。
初始化金丝雀值:接下来,在 mov %fs:0x28,%rax 以及 mov %rax,-0x8(%rbp) 这两个命令是不是看起来感觉很奇怪,这一步其实在C代码里面根本就不存在,这是编译器做的一种保护,在栈空间 -0x8(%rbp) 这个地址放上一个“金丝雀值”,在函数返回的时候查看这个值是否被改变以确定栈空间是否被损坏,在后续会详细介绍这个机制。
一条没用的指令:继续向下看xor %eax,%eax 这条指令将eax寄存器清零了,从汇编代码继续向后看rax寄存器会写入新的值,覆盖掉这个操作,这个清空操作后也没有将eax值写入内存或mov入其他寄存器,所以这个指令可以直接忽略。
初始化局部变量:接下来0x00000000000011f0 ~ 0x0000000000001215 这段内容很有特点,都是往内存里面写入立即数,而且写入得地址都是相对栈帧基址向下某个地址上得值,这段其实就是在给局部变量赋值,配合c代码的操作,我们可以推测出 -0x28(%rbp) 这个地址存放的值就是 a,后续的可以用相同的方式推测出-0x2a(%rbp) 存放的是局部变量b, -0x2b(%rbp) 存放的是 c,-0x20(%rbp)存放的是 d,-0x18(%rbp) 存放 e, -0x10(%rbp) 存放 f。其中有两条指令值得我们研究 lea -0x2b(%rbp),%rax 以及 mov %rax,-0x10(%rbp) ,lea这个指令读者可以学习一下,其实就是将某个寻址计算的结果放入某个寄存器里面,在这条指令里就是将 -0x2b(%rbp) 计算得到地址放入 rax 寄存器里面,然后将这个地址再放入内存中,而-0x2b(%rbp) 这个地址放的是什么东西,通过我们之前的分析方法可以确定这个地址被初始化后放的是0x72,被初始化为0x72的是其实是局部变量c,也就是局部变量c的地址被放入了内存中。计算某个变量的地址,这就是c语言里面的取址符&的规则,这两个指令其实对应的就是c代码里面 void *f = &c 这一行。
根据ABI构建形参:至此,函数调用前的局部变量全部初始化完成,c代码里面接下来就是函数调用了,既然要函数调用,就要构造符合ABI规定的形参存储位置,因此我们整体上可以推测0x0000000000001219 ~ 0x0000000000001239这段指令是在构造形参。movzbl -0x2b(%rbp),%eax 与 movsbl %al,%edx 这两条指令将内存里面某个参数放入了edx寄存器里面,我们回头看X86_64 abi 规则 rdx 存放的应该是第三个参数,回头看c语言里面函数调用第三个参数是char类型变量c,通过我们之前分析 -0x2b(%rbp) 这个地址存放的就是变量c,第三个形参构建完成;之后将 -0x2a(%rbp) 的值放入 esi 寄存器里面,rsi 寄存器放的是第二个形参,而 -0x2a(%rbp) 就是第二个实参 b 的地址,第二个形参构建完成; 类似的,接下来几条指令依次是下面这个顺序,将 f 的值放入 r8 ,将 e 的值放入 rdi,将 d 的值放入 rcx ,将 a 的值放入eax里,将 f 的值由 r8 移到 r9,将 e 的值由 rdi 移动到 r8,将 a 的值由 eax 移动到 rdi。最终a 在 rdi,b 在 rsi,c 在 rdx, d 在 rcx,e 在r8,f 在 r9,对照 c 代码以及abi规则,完全能对应的上。
获取返回值:根据ABI规则,执行callq命令调用func函数执行完成后返回值放在rax上,但是我们的返回值类型是 int 只占4个字节,所以将mov的是四字节将eax放到-0x24(%rbp) 这个地址,对照c代码,这个地址放的其实就是ret变量。
构造main返回值: main函数返回值是ret,存放在-0x24(%rbp)这个地址,从这个地址取出值,根据ABI规则放到eax里面。
检查金丝雀值是否发生变化: 之前我们分析 -0x8(%rbp) 存放的就是金丝雀值,现在重新取出,和放入时候的值重新对比,如果发生了变化就跳转到 stack_chk_fail@plt 来执行相关后续操作了,一旦执行stack_chk_fail@plt这个函数,操作系统会向当前进程发出信号,使进程终止。
回收栈内存: leaveq这个指令直接执行了两步,首先将rbp栈帧基址指向的内存的值放入rsp里面,然后自动将rsp指向内存的值放入rbp。这个步骤其实就直接回收了当前函数开辟的栈内存,栈帧基址指针恢复到调用main函数前的状态,只有栈帧基址有效的前提下这个操作才有效,如果rbp没有被用作栈帧基址,还是需要通过对rsp减过的值加回来,push过的值pop出来才能恢复上一个栈帧。
返回上个函数继续执行: retq指令又从栈顶取出上个函数下一条指令的地址,放入 rip 里面,这就回到了上个函数的流程,继续向下执行。
看到这里读者可能有下面两个疑问:
在上一小节已经逐条汇编指令分析了程序的行为,本小节简单过一遍相关内容(指令地址 + 这段指令的含义)。
值得注意的是printf函数入参的个数达到了7个,超过了6个入参,在这种情况下将第7个参数放入栈上,而栈是要求需要按照16字节对齐,所以在 0x00000000000011bf 这个地址出现了sub $0x8,%rsp 这个指令,目的就是对齐栈内存。
当我们理解了汇编函数的行为后,其实栈内存理解起来就变得信手拈来了,栈空间上存储的内容其实和汇编指令中涉及到操作rsp指针的指令都是能一一对应起来的,接下来我们分析一下停在func函数入口时栈内存分布情况。
(gdb) b func
Breakpoint 1 at 0x555555555169: file abi.c, line 5.
(gdb) r
Starting program: /home/w/Desktop/usrProjs/stack_analise/abi
Breakpoint 1, func (a=0, b=-18689, c=85 'U', d=0, e=0, f=0x0) at abi.c:5
warning: Source file is more recent than executable.
5 {
(gdb) x/100xb $rsp
0x7fffffffd7b8: 0x40 0x52 0x55 0x55 0x55 0x55 0x00 0x00
0x7fffffffd7c0: 0xe8 0x42 0xfb 0xf7 0xff 0x72 0x02 0x00
0x7fffffffd7c8: 0x01 0x00 0x00 0x00 0x55 0x55 0x00 0x00
0x7fffffffd7d0: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7d8: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7e0: 0xc5 0xd7 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffd7e8: 0x00 0xea 0x0d 0x13 0xe0 0x5d 0xda 0x5a
0x7fffffffd7f0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7f8: 0x83 0x70 0xde 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd800: 0x20 0xc6 0xff 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd808: 0xe8 0xd8 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffd810: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00
0x7fffffffd818: 0xd5 0x51 0x55 0x55
(gdb) disassemble
Dump of assembler code for function func:
=> 0x0000555555555169 <+0>: endbr64
0x000055555555516d <+4>: push %rbp
0x000055555555516e <+5>: mov %rsp,%rbp
0x0000555555555171 <+8>: sub $0x40,%rsp
0x0000555555555175 <+12>: mov %edi,-0x14(%rbp)
0x0000555555555178 <+15>: mov %esi,%eax
0x000055555555517a <+17>: mov %rcx,-0x28(%rbp)
0x000055555555517e <+21>: mov %r8,-0x30(%rbp)
0x0000555555555182 <+25>: mov %r9,-0x38(%rbp)
0x0000555555555186 <+29>: mov %ax,-0x18(%rbp)
0x000055555555518a <+33>: mov %edx,%eax
0x000055555555518c <+35>: mov %al,-0x1c(%rbp)
0x000055555555518f <+38>: movl $0x0,-0x4(%rbp)
0x0000555555555196 <+45>: movsbl -0x1c(%rbp),%ecx
0x000055555555519a <+49>: movswl -0x18(%rbp),%edx
0x000055555555519e <+53>: mov -0x30(%rbp),%rdi
0x00005555555551a2 <+57>: mov -0x28(%rbp),%rsi
0x00005555555551a6 <+61>: mov -0x14(%rbp),%eax
0x00005555555551a9 <+64>: sub $0x8,%rsp
0x00005555555551ad <+68>: pushq -0x38(%rbp)
0x00005555555551b0 <+71>: mov %rdi,%r9
0x00005555555551b3 <+74>: mov %rsi,%r8
0x00005555555551b6 <+77>: mov %eax,%esi
0x00005555555551b8 <+79>: lea 0xe49(%rip),%rdi # 0x555555556008
0x00005555555551bf <+86>: mov $0x0,%eax
0x00005555555551c4 <+91>: callq 0x555555555070 <printf@plt>
0x00005555555551c9 <+96>: add $0x10,%rsp
0x00005555555551cd <+100>: mov %eax,-0x4(%rbp)
0x00005555555551d0 <+103>: mov -0x4(%rbp),%eax
0x00005555555551d3 <+106>: leaveq
0x00005555555551d4 <+107>: retq
End of assembler dump.
(gdb) fr 1
#1 0x0000555555555240 in main () at abi.c:21
21 int ret = func(a, b, c, d, e, f);
(gdb) disassemble
Dump of assembler code for function main:
0x00005555555551d5 <+0>: endbr64
0x00005555555551d9 <+4>: push %rbp
0x00005555555551da <+5>: mov %rsp,%rbp
0x00005555555551dd <+8>: sub $0x30,%rsp
0x00005555555551e1 <+12>: mov %fs:0x28,%rax
0x00005555555551ea <+21>: mov %rax,-0x8(%rbp)
0x00005555555551ee <+25>: xor %eax,%eax
0x00005555555551f0 <+27>: movl $0x1,-0x28(%rbp)
0x00005555555551f7 <+34>: movw $0x2,-0x2a(%rbp)
0x00005555555551fd <+40>: movb $0x72,-0x2b(%rbp)
0x0000555555555201 <+44>: movq $0x3,-0x20(%rbp)
0x0000555555555209 <+52>: movq $0x4,-0x18(%rbp)
0x0000555555555211 <+60>: lea -0x2b(%rbp),%rax
0x0000555555555215 <+64>: mov %rax,-0x10(%rbp)
0x0000555555555219 <+68>: movzbl -0x2b(%rbp),%eax
0x000055555555521d <+72>: movsbl %al,%edx
0x0000555555555220 <+75>: movswl -0x2a(%rbp),%esi
0x0000555555555224 <+79>: mov -0x10(%rbp),%r8
0x0000555555555228 <+83>: mov -0x18(%rbp),%rdi
0x000055555555522c <+87>: mov -0x20(%rbp),%rcx
0x0000555555555230 <+91>: mov -0x28(%rbp),%eax
0x0000555555555233 <+94>: mov %r8,%r9
0x0000555555555236 <+97>: mov %rdi,%r8
0x0000555555555239 <+100>: mov %eax,%edi
0x000055555555523b <+102>: callq 0x555555555169 <func>
=> 0x0000555555555240 <+107>: mov %eax,-0x24(%rbp)
0x0000555555555243 <+110>: mov -0x24(%rbp),%eax
0x0000555555555246 <+113>: mov -0x8(%rbp),%rdx
0x000055555555524a <+117>: xor %fs:0x28,%rdx
0x0000555555555253 <+126>: je 0x55555555525a <main+133>
0x0000555555555255 <+128>: callq 0x555555555060 <__stack_chk_fail@plt>
0x000055555555525a <+133>: leaveq
0x000055555555525b <+134>: retq
End of assembler dump.
(gdb) fr 1
#1 0x0000555555555240 in main () at abi.c:21
21 int ret = func(a, b, c, d, e, f);
(gdb) info locals
a = 1
b = 2
c = 114 'r'
d = 3
e = 4
f = 0x7fffffffd7c5
ret = 21845
(gdb) p /x &a
$1 = 0x7fffffffd7c8
(gdb) p /x &b
$2 = 0x7fffffffd7c6
(gdb) p /x &c
$3 = 0x7fffffffd7c5
(gdb) p /x &d
$4 = 0x7fffffffd7d0
(gdb) p /x &e
$5 = 0x7fffffffd7d8
(gdb) p /x &f
$6 = 0x7fffffffd7e0
(gdb) p /x &ret
$7 = 0x7fffffffd7cc
(gdb) p $rbp-0x8
$8 = (void *) 0x7fffffffd7e8
栈内存内容分析:
对栈空间的分析到此为止读者有兴趣可以继续分析func函数的栈帧结构,或者自己写出一些c代码来进行分析。
我们将这个相同的文件以O1的优化等级进行编译看看反汇编会发生什么样的变化。
(gdb) disassemble main
Dump of assembler code for function main:
0x000000000000119f <+0>: endbr64
0x00000000000011a3 <+4>: push %rbx
0x00000000000011a4 <+5>: sub $0x10,%rsp
0x00000000000011a8 <+9>: mov $0x28,%ebx
0x00000000000011ad <+14>: mov %fs:(%rbx),%rax
0x00000000000011b1 <+18>: mov %rax,0x8(%rsp)
0x00000000000011b6 <+23>: xor %eax,%eax
0x00000000000011b8 <+25>: movb $0x72,0x7(%rsp)
0x00000000000011bd <+30>: lea 0x7(%rsp),%r9
0x00000000000011c2 <+35>: mov $0x4,%r8d
0x00000000000011c8 <+41>: mov $0x3,%ecx
0x00000000000011cd <+46>: mov $0x72,%edx
0x00000000000011d2 <+51>: mov $0x2,%esi
0x00000000000011d7 <+56>: mov $0x1,%edi
0x00000000000011dc <+61>: callq 0x1169 <func>
0x00000000000011e1 <+66>: mov 0x8(%rsp),%rdx
0x00000000000011e6 <+71>: xor %fs:(%rbx),%rdx
0x00000000000011ea <+75>: jne 0x11f2 <main+83>
0x00000000000011ec <+77>: add $0x10,%rsp
0x00000000000011f0 <+81>: pop %rbx
0x00000000000011f1 <+82>: retq
0x00000000000011f2 <+83>: callq 0x1060 <__stack_chk_fail@plt>
End of assembler dump.
(gdb) disassemble func
Dump of assembler code for function func:
0x0000000000001169 <+0>: endbr64
0x000000000000116d <+4>: sub $0x8,%rsp
0x0000000000001171 <+8>: mov %rcx,%rax
0x0000000000001174 <+11>: movswl %si,%ecx
0x0000000000001177 <+14>: push %r9
0x0000000000001179 <+16>: push %r8
0x000000000000117b <+18>: mov %rax,%r9
0x000000000000117e <+21>: movsbl %dl,%r8d
0x0000000000001182 <+25>: mov %edi,%edx
0x0000000000001184 <+27>: lea 0xe7d(%rip),%rsi # 0x2008
0x000000000000118b <+34>: mov $0x1,%edi
0x0000000000001190 <+39>: mov $0x0,%eax
0x0000000000001195 <+44>: callq 0x1070 <__printf_chk@plt>
0x000000000000119a <+49>: add $0x18,%rsp
0x000000000000119e <+53>: retq
End of assembler dump.
通过反汇编是不是感觉函数变短了很多。细看我们可以看到,main函数和func函数开头都没有了对rbp指针的入栈操作,同时函数中对rsp指针减的值变少了很多,我们来大致分析一下。
我们接下来尝试破坏一下金丝雀值来看看会有什么事发生,我们先就用当前的程序使用gdb强行破坏一下这个值看看现象。
(gdb) b func
Breakpoint 1 at 0x1169: file abi.c, line 5.
(gdb) r
Starting program: /home/w/Desktop/usrProjs/stack_analise/abi
Breakpoint 1, func (a=0, b=-18689, c=85 'U', d=0, e=0, f=0x0) at abi.c:5
5 {
(gdb) bt
#0 func (a=0, b=-18689, c=85 'U', d=0, e=0, f=0x0) at abi.c:5
#1 0x0000555555555240 in main () at abi.c:21
(gdb) fr 1
#1 0x0000555555555240 in main () at abi.c:21
21 int ret = func(a, b, c, d, e, f);
(gdb) disassemble
Dump of assembler code for function main:
0x00005555555551d5 <+0>: endbr64
0x00005555555551d9 <+4>: push %rbp
0x00005555555551da <+5>: mov %rsp,%rbp
0x00005555555551dd <+8>: sub $0x30,%rsp
0x00005555555551e1 <+12>: mov %fs:0x28,%rax
0x00005555555551ea <+21>: mov %rax,-0x8(%rbp)
0x00005555555551ee <+25>: xor %eax,%eax
0x00005555555551f0 <+27>: movl $0x1,-0x28(%rbp)
0x00005555555551f7 <+34>: movw $0x2,-0x2a(%rbp)
0x00005555555551fd <+40>: movb $0x72,-0x2b(%rbp)
0x0000555555555201 <+44>: movq $0x3,-0x20(%rbp)
0x0000555555555209 <+52>: movq $0x4,-0x18(%rbp)
0x0000555555555211 <+60>: lea -0x2b(%rbp),%rax
0x0000555555555215 <+64>: mov %rax,-0x10(%rbp)
0x0000555555555219 <+68>: movzbl -0x2b(%rbp),%eax
0x000055555555521d <+72>: movsbl %al,%edx
0x0000555555555220 <+75>: movswl -0x2a(%rbp),%esi
0x0000555555555224 <+79>: mov -0x10(%rbp),%r8
0x0000555555555228 <+83>: mov -0x18(%rbp),%rdi
0x000055555555522c <+87>: mov -0x20(%rbp),%rcx
0x0000555555555230 <+91>: mov -0x28(%rbp),%eax
0x0000555555555233 <+94>: mov %r8,%r9
0x0000555555555236 <+97>: mov %rdi,%r8
0x0000555555555239 <+100>: mov %eax,%edi
0x000055555555523b <+102>: callq 0x555555555169 <func>
=> 0x0000555555555240 <+107>: mov %eax,-0x24(%rbp)
0x0000555555555243 <+110>: mov -0x24(%rbp),%eax
0x0000555555555246 <+113>: mov -0x8(%rbp),%rdx
0x000055555555524a <+117>: xor %fs:0x28,%rdx
0x0000555555555253 <+126>: je 0x55555555525a <main+133>
0x0000555555555255 <+128>: callq 0x555555555060 <__stack_chk_fail@plt>
0x000055555555525a <+133>: leaveq
0x000055555555525b <+134>: retq
End of assembler dump.
(gdb) x/gx $rbp-8
0x7fffffffd7e8: 0x5b5c38f668b36600
(gdb) set {long long}($rbp - 0x8) = 0x0
(gdb) x/gx $rbp-8
0x7fffffffd7e8: 0x0000000000000000
(gdb) b __stack_chk_fail
__stack_chk_fail __stack_chk_fail@plt __stack_chk_fail_local
(gdb) b __stack_chk_fail
Breakpoint 2 at 0x7ffff7ef2c90: file stack_chk_fail.c, line 23.
(gdb) c
Continuing.
a: 1, b: 2, c: r, d: 3, e: 4, f: 0x7fffffffd7c5
Breakpoint 2, __stack_chk_fail () at stack_chk_fail.c:23
23 stack_chk_fail.c: No such file or directory.
(gdb) c
Continuing.
*** stack smashing detected ***: terminated
Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
可以看出金丝雀值被破坏后程序确实跳转到 __stack_chk_fail 函数执行,运行到最后进程接收到了SIGABRT信号,进程终止。
缓冲区溢出
金丝雀值被破坏比较常见的情形源自于缓冲区溢出,栈上给定一个数组,往里面填入值,如果不注意就容易出现数组越界的情况,这是任何一个C/C++程序员不愿意看到的情况。我们来编写一个简单的函数来触发一下缓冲区溢出的场景。
#include <stdio.h>
#include <string.h>
int main()
{
char *str = "overflow";
char buf[5];
sprintf(buf, "%s", str);
return 0;
}
使用gcc -O0 -g overflow.c -o overflow 命令编译该文件后,用gdb开始调试
(gdb) b sprintf
Breakpoint 1 at 0x1070
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000001169 <+0>: endbr64
0x000000000000116d <+4>: push %rbp
0x000000000000116e <+5>: mov %rsp,%rbp
0x0000000000001171 <+8>: sub $0x20,%rsp
0x0000000000001175 <+12>: mov %fs:0x28,%rax
0x000000000000117e <+21>: mov %rax,-0x8(%rbp)
0x0000000000001182 <+25>: xor %eax,%eax
0x0000000000001184 <+27>: lea 0xe79(%rip),%rax # 0x2004
0x000000000000118b <+34>: mov %rax,-0x18(%rbp)
0x000000000000118f <+38>: mov -0x18(%rbp),%rdx
0x0000000000001193 <+42>: lea -0xd(%rbp),%rax
0x0000000000001197 <+46>: lea 0xe6f(%rip),%rsi # 0x200d
0x000000000000119e <+53>: mov %rax,%rdi
0x00000000000011a1 <+56>: mov $0x0,%eax
0x00000000000011a6 <+61>: callq 0x1070 <sprintf@plt>
0x00000000000011ab <+66>: mov $0x0,%eax
0x00000000000011b0 <+71>: mov -0x8(%rbp),%rcx
0x00000000000011b4 <+75>: xor %fs:0x28,%rcx
0x00000000000011bd <+84>: je 0x11c4 <main+91>
0x00000000000011bf <+86>: callq 0x1060 <__stack_chk_fail@plt>
0x00000000000011c4 <+91>: leaveq
0x00000000000011c5 <+92>: retq
End of assembler dump.
(gdb) b __stack_chk_fail
Breakpoint 2 at 0x1060
(gdb) r
Starting program: /home/w/Desktop/usrProjs/stack_analise/overflow
Breakpoint 1, __sprintf (s=0x7fffffffd7d3 "\377\377\177", format=0x55555555600d "%s") at sprintf.c:25
25 sprintf.c: No such file or directory.
(gdb) fr 1
#1 0x00005555555551ab in main () at overflow.c:8
8 sprintf(buf, "%s", str);
(gdb) x/50xb $rsp
0x7fffffffd7c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7c8: 0x04 0x60 0x55 0x55 0x55 0x55 0x00 0x00
0x7fffffffd7d0: 0xd0 0xd8 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffd7d8: 0x00 0xea 0xdc 0x24 0x3f 0xda 0x0e 0xd5
0x7fffffffd7e0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7e8: 0x83 0x70 0xde 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd7f0: 0x20 0xc6
(gdb) x/gx $rbp-0x8
0x7fffffffd7d8: 0xd50eda3f24dcea00
(gdb) p buf
$1 = "\377\377\177\000"
(gdb) p &buf
$2 = (char (*)[5]) 0x7fffffffd7d3
(gdb) p &str
$3 = (char **) 0x7fffffffd7c8
(gdb) c
Continuing.
Breakpoint 2, __stack_chk_fail () at stack_chk_fail.c:23
23 stack_chk_fail.c: No such file or directory.
(gdb) bt
#0 __stack_chk_fail () at stack_chk_fail.c:23
#1 0x00005555555551c4 in main () at overflow.c:10
(gdb) fr 1
#1 0x00005555555551c4 in main () at overflow.c:10
10 }
(gdb) x/50xb $rsp
0x7fffffffd7c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7c8: 0x04 0x60 0x55 0x55 0x55 0x55 0x00 0x00
0x7fffffffd7d0: 0xd0 0xd8 0xff 0x6f 0x76 0x65 0x72 0x66
0x7fffffffd7d8: 0x6c 0x6f 0x77 0x00 0x3f 0xda 0x0e 0xd5
0x7fffffffd7e0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7e8: 0x83 0x70 0xde 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd7f0: 0x20 0xc6
(gdb) p buf
$4 = "overf"
(gdb) x/gx $rbp-0x8
0x7fffffffd7d8: 0xd50eda3f00776f6c
(gdb) c
Continuing.
*** stack smashing detected ***: terminated
Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb)
我们分别在sprintf 函数逻辑执行前和触发金丝雀值破坏后执行的 __stack_chk_fail 函数打上断点,在我们可以,清晰的观察到 sprintf 函数执行前后金丝雀值由 0xd50eda3f24dcea00 变为了 0xd50eda3f00776f6c。我们先画出栈内存布局
0x7fffffffd7c8~0x7fffffffd7cf str字符串指针对应地址(8个字节)
0x7fffffffd7d0~0x7fffffffd7d2 为了内存对齐填充的空内容,没用到的(3个字节)
0x7fffffffd7d3~0x7fffffffd7d7 buf数组在内存中的位置(5个字节)
0x7fffffffd7d8~0x7fffffffd7df 金丝雀值在内存中的位置 (8个字节)
当运行sprintf函数时,会从buf数组开始的位置,向高地址依次填入“overflow” 这个字符串,算上末尾的结束符需要9个字节,最后就会导致内存越界,刚好修改了金丝雀值的一部分。我们对比一下金丝雀前后发生了什么变化,刚好就是buf缓冲器放不下的东西就放在了金丝雀值上。
地址 初始值 修改后的值
0x7fffffffd7d8 0x00 0x6c ('l' 的ascii码)
0x7fffffffd7d9 0xea 0x6f ('o' 的ascii码)
0x7fffffffd7da 0xdc 0x77 ('w' 的ascii码)
0x7fffffffd7db 0x24 0x00 ('\0' 的ascii码)
0x7fffffffd7dc 0x3f 0x3f
0x7fffffffd7dd 0xda 0xda
0x7fffffffd7de 0x0e 0x0e
0x7fffffffd7df 0xd5 0xd5
没有金丝雀值会发生什么事
金丝雀值放入栈上后其实再往高低址看就是栈帧基址以及为了保存上个函数的现场而放置的程序计数器的值,如果不对金丝雀值做判断,在函数返回过程中不会调用__stack_chk_fail 这个函数,此时如果程序计数器的值被修改,后续一个不确定的地址放到rip寄存器里,指向一个未知的内存区域,比较好的情况是这段区域的内存是不可执行的,程序后续直接崩溃,而不妙的情况是这段区域的内存正好是攻击者放入的攻击代码的区域,那么攻击者就直接将这个线程给劫持了,他可以做自己想做的事情。金丝雀值就是为了防止这种情况的出现。
本文描述了一种分析汇编与栈内存的方法论,并以x86_64架构为例,以一个简单的程序为,以此方法论为基础,分析了程序行为以及栈空间中各种变量的分布。同时简要分析了一下编译器优化等级的提升对程序带来的影响。这套抽象出来的方法论在其他处理器架构中同样适用,这篇文章写的实在是太长了,后续作者可能新开一个博客,利用这个方法论来分析 arm / risc-v 的反汇编以及栈内存,(arm汇编很早之前了解过,作者已经忘光了,risc-v的汇编作者更是没接触过,希望这不是给自己挖了但是填不上的坑吧),后续一遍学习一边分析。
《深入理解计算机系统》
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。