计算机科学基础知识之(六):理解栈帧
作者:linuxer 发布于:2015-3-12 13:00 分类:基础学科
一、前言
本文以一个简单的例子来描述ARM linux下的stack frame。
本文也是对tigger网友问题的回复。
二、源代码
#include <stdio.h>
static int static_interface_leaf( int x, int y )
{
int tmp0 = 0x12;
int tmp1 = 0x34;
int tmp2 = 0x56;tmp0 = x;
tmp1 = y;return (tmp0+tmp1+tmp2);
}int public_interface_leaf( int x, int y )
{
int tmp0 = 0x12;
int tmp1 = 0x34;
int tmp2 = 0x56;tmp0 = x;
tmp1 = y;return (tmp0+tmp1+tmp2);
}void public_interface( int x )
{
int tmp0 = 0x12;
int tmp1 = 0x34;tmp0 = x;
public_interface_leaf( tmp0, tmp1 );
static_interface_leaf( tmp0, tmp1 );
}int main(int argc, char **argv)
{
int tmp0 = 0x12;public_interface( tmp0 );
return 0;
}
三、逐级stack frame分析
1、准备知识
根据AAPCS的描述,stack是full-descending并且需要满足两种约束:一种是通用约束,适用所有的场景,另外一种是针对public interface的约束。通用约束有3条:
(1)SP只能访问stack base和stack limit之间的memory,即Stack-limit < SP <= stack-base
(2)SP必须对齐在4个字节上,即SP mod 4 = 0
(3)函数只能访问自己能回溯的那些栈帧。例如f1调用f2,而f2函数又调用了f3,那么f3是可以访问自己的stack以及f2和f1的stack,也就是说,函数可以访问[SP, stack-base – 1]之间的内容
对public interface的约束多了一条,就是SP必须对齐在8个字节上,即SP mod 8 = 0
关于ARM的ABI,还有一份文档,IHI0046B_ABI_Advisory_1,这份文件中讲到,在调用所有的AAPCS兼容的函数的时候都要求SP是对齐在8个字节上。
2、起始点的用户栈的情况
在静态链接文档中,我们说过,函数的入口函数不是main函数而是_start函数,调用序列是_start()->__libc_start_main()->main()。main函数之前对于所有的程序都是一样的,因此不需要每一个程序员都重复进行那些动作,因此留给程序员一个main函数的入口,开始自己相关逻辑的处理。内核在start函数(我在这里以及后面的文档中省略了下划线)之前的stack frame并不是空的,内核会创建一些资料在stack上,具体如下:
具体怎么在用户栈上建立上面的数据结构,有兴趣的同学可以参考内核的create_elf_tables函数。此外,需要提醒的是这些数据内容虽然在栈上,但是不是stack frame的一部分,有点类似内核空间到用户空间参数传递的味道。为何这么说呢?因为在start函数中有一条汇编指令:mov fp, #0,该指令清除frame pointer,在debugger做栈的回溯的时候,当fp等于0的时候也就意味着到了最外层函数。
3、start函数的start frame
0000829c <_start>:
829c: e59fc024 ldr ip, [pc, #36] ; 82c8 <.text+0x2c>
82a0: e3a0b000 mov fp, #0 ; 0x0--------最外层函数,清除frame pointer
82a4: e49d1004 ldr r1, [sp], #4----------r1 = argc, sp=sp+4,sp指向了argv[]
82a8: e1a0200d mov r2, sp----------r2保存了stack end,也就是argv[]那个位置
82ac: e52d2004 str r2, [sp, #-4]!--------将stack end压入栈
82b0: e52d0004 str r0, [sp, #-4]!--------将rtld_fini压入栈
82b4: e59f0010 ldr r0, [pc, #16] ; 82cc <.text+0x30>
82b8: e59f3010 ldr r3, [pc, #16] ; 82d0 <.text+0x34>
82bc: e52dc004 str ip, [sp, #-4]!--------将fini压入栈
82c0: ebffffef bl 8284 <.text-0x18>-------call __libc_start_main
82c4: ebffffeb bl 8278 <.text-0x24>
82c8: 0000848c .word 0x0000848c
82cc: 00008454 .word 0x00008454
82d0: 00008490 .word 0x00008490
在调用__libc_start_main函数之前,stack frame的情况如下:
大家可以对照上面的汇编和图片,我这里只是描述基本知识点:
1、stack的确是full-descending的,SP指向了start函数的顶部,下一个函数必须先减SP,才能保存其栈上的数据。
2、内核到用户空间当然是public interface,因此在进入start函数的时候SP当前是8字节对齐。而start函数的栈有3个变量共计12个字节,在调用__libc_start_main函数这个public interface的时候当然也要8字节对齐,按理说这里start函数有一个小小的4字节的空洞,但实际上,代码是抹去了用户栈的argc这个参数,因此start的栈的细节如下:
虽然抹去了用户栈的argc这个参数,不过没有关系,反正它已经保存在了r1寄存器中了。
4、__libc_start_main函数的stack frame
__libc_start_main是libc定义的符号,我们动态链接的时候,这些代码没有进入我们测试的ELF文件。这里略过吧,毕竟查阅c库代码也是非常烦人的事情。
5、main函数的stack frame
00008454
:
8454: e92d4800 stmdb sp!, {fp, lr}---将上一个函数的 fp和lr寄存器压入stack, sp=sp-8
8458: e28db004 add fp, sp, #4 ; ---上一个函数的sp+4就是本函数stack frame的开始
845c: e24dd010 sub sp, sp, #16 ; 0x10
8460: e1a03000 mov r3, r0
8464: e50b1014 str r1, [fp, #-20]------保存argv
8468: e54b300d str r3, [fp, #-16]------保存argc
846c: e3a03012 mov r3, #18 ; 0x12---tmp0 = 0x12,[fp, #-8]就是源代码的tmp0
8470: e50b3008 str r3, [fp, #-8]
8474: e51b0008 ldr r0, [fp, #-8]-----传递tmp0参数
8478: ebffffe3 bl 840c
847c: e3a03000 mov r3, #0 ; 0x0
8480: e1a00003 mov r0, r3
8484: e24bd004 sub sp, fp, #4 ; 0x4
8488: e8bd8800 ldmia sp!, {fp, pc}
在调用public_interface之前,main函数的stack frame如下:
对照代码和图片,我们有下面的解释:
(1)第一条指令就是stmdb,这里db就是decrease before的意思,再次确认stack的确是full-descending的
(2)虽然只有一个临时变量tmp0,但是编译器还是传递了argc和argv这两个参数,具体为何我也没有考虑清楚,因此在分配main的stack frame的时候使用了sub sp, sp, #16,分配4个int型数据,当然是为了对齐8字节。
(3)在一个函数的执行过程中,sp和fp之间就是该函数的stack frame。sp指向stack frame的顶部(低地址),fp指向顶部。
(4)由于main函数的fp加4就是__libc_start_main的sp,因此在main函数的stack上不需要保存其sp,只要保存fp就OK了。
6、public_interface的stack frame
0000840c
:
840c: e92d4800 stmdb sp!, {fp, lr}
8410: e28db004 add fp, sp, #4 ; 0x4
8414: e24dd010 sub sp, sp, #16 ; 0x10
8418: e50b0010 str r0, [fp, #-16]---------中间变量,保存传入的x参数
841c: e3a03012 mov r3, #18 ; 0x12
8420: e50b300c str r3, [fp, #-12]---------tmp0 = 0x12
8424: e3a03034 mov r3, #52 ; 0x34
8428: e50b3008 str r3, [fp, #-8]----------tmp1 = 0x34
842c: e51b3010 ldr r3, [fp, #-16]
8430: e50b300c str r3, [fp, #-12]---------tmp0 = x
8434: e51b000c ldr r0, [fp, #-12]
8438: e51b1008 ldr r1, [fp, #-8]
843c: ebffffda bl 83ac
8440: e51b000c ldr r0, [fp, #-12]
8444: e51b1008 ldr r1, [fp, #-8]
8448: ebffffbf bl 834c
844c: e24bd004 sub sp, fp, #4 ; 0x4
8450: e8bd8800 ldmia sp!, {fp, pc}
栈帧情况如下:
这里比较简单,大家自行分析就OK了。
7、调用static函数
根据AAPCS的描述,只有public接口才需要SP 8字节对齐。不过测试程序表明所有的都是8字节对齐的,我的编译器关于ABI的缺省设定是-mabi=aapcs-linux,猜想可能是所有的函数都被编译成AAPCS-comforming fuction。具体大家可以自己写代码练习一下。
参考文献
1、AAPCS。Procedure Call Standard for the ARM Architecture
2、IHI0046B_ABI_Advisory_1。ABI for the ARM Architecture Advisory Note – SP must be 8-byte aligned on entry to AAPCS-conforming functions
原创文章,转发请注明出处。蜗窝科技
http://www.wowotech.net/basic_subject/stack-frame.html

评论:
2017-02-13 09:05
2015-03-13 00:14
A()
{
...
B()
...
}
在B函数的一开始就会把fp_A压入B函数的stack,并且fp_B = sp_A - 4。
对于B函数而言,sp_B和fp_B之间的内容就是B函数的stack frame,在B函数中想回溯上一级函数(也就是A函数)其实就是求sp_A和fp_A,fp_A在B的stack frame上,访问[fp_B, #-4]就可获取fp_A,而sp_A = fp_B + 4
如果有更深入层次的函数调用,重复上面的过程,就完成了栈的回溯。当fp等于0的时候,意外着来到最外层的函数
2015-03-12 15:34
1:8454: e92d4800 stmdb sp!, {fp, lr}---将上一个函数的 fp和lr寄存器压入stack, sp=sp+8
这里应该笔误了吧,sp=sp-8
2:8468: e54b300d strb r3, [fp, #-13]------保存argc
这里应该是#-16?
3:不知道有没有手动unwind过一个堆栈,比如我现在知道一个地址是当前的函数的sp,
但是我想从这个sp的地址,不断的查看call stack,怎么样能找到这一个函数跟另一函数的交接处呢?
2015-03-12 18:32
这里应该笔误了吧,sp=sp-8
--------------------------
的确是写错了,应该是向低地址方向移动sp指针
2:8468: e54b300d strb r3, [fp, #-13]------保存argc
这里应该是#-16?
----------------------------------
^_^,我的程序有一个小错误,我鬼使神差的把int argc写成char argc了,如果是int argc,当然应该是#-16
3:不知道有没有手动unwind过一个堆栈,比如我现在知道一个地址是当前的函数的sp,
但是我想从这个sp的地址,不断的查看call stack,怎么样能找到这一个函数跟另一函数的交接处呢?
--------------------------------
如果知道当前函数的sp和fp,栈的回溯很简单,通过fp+4可以知道外层函数的顶部(sp),而底部(fp),可以pop current stack获得。
功能
最新评论
- wangjing
写得太好了 - wangjing
写得太好了! - DRAM
圖面都沒辦法顯示出來好像掛點了。 - Simbr
bus至少是不是还有个subsystem? - troy
@testtest:只要ldrex-modify-strex... - gh
Linux 内核在 sparse 内存模型基础上实现了vme...
文章分类
随机文章
文章存档
- 2025年4月(5)
- 2024年2月(1)
- 2023年5月(1)
- 2022年10月(1)
- 2022年8月(1)
- 2022年6月(1)
- 2022年5月(1)
- 2022年4月(2)
- 2022年2月(2)
- 2021年12月(1)
- 2021年11月(5)
- 2021年7月(1)
- 2021年6月(1)
- 2021年5月(3)
- 2020年3月(3)
- 2020年2月(2)
- 2020年1月(3)
- 2019年12月(3)
- 2019年5月(4)
- 2019年3月(1)
- 2019年1月(3)
- 2018年12月(2)
- 2018年11月(1)
- 2018年10月(2)
- 2018年8月(1)
- 2018年6月(1)
- 2018年5月(1)
- 2018年4月(7)
- 2018年2月(4)
- 2018年1月(5)
- 2017年12月(2)
- 2017年11月(2)
- 2017年10月(1)
- 2017年9月(5)
- 2017年8月(4)
- 2017年7月(4)
- 2017年6月(3)
- 2017年5月(3)
- 2017年4月(1)
- 2017年3月(8)
- 2017年2月(6)
- 2017年1月(5)
- 2016年12月(6)
- 2016年11月(11)
- 2016年10月(9)
- 2016年9月(6)
- 2016年8月(9)
- 2016年7月(5)
- 2016年6月(8)
- 2016年5月(8)
- 2016年4月(7)
- 2016年3月(5)
- 2016年2月(5)
- 2016年1月(6)
- 2015年12月(6)
- 2015年11月(9)
- 2015年10月(9)
- 2015年9月(4)
- 2015年8月(3)
- 2015年7月(7)
- 2015年6月(3)
- 2015年5月(6)
- 2015年4月(9)
- 2015年3月(9)
- 2015年2月(6)
- 2015年1月(6)
- 2014年12月(17)
- 2014年11月(8)
- 2014年10月(9)
- 2014年9月(7)
- 2014年8月(12)
- 2014年7月(6)
- 2014年6月(6)
- 2014年5月(9)
- 2014年4月(9)
- 2014年3月(7)
- 2014年2月(3)
- 2014年1月(4)
2017-02-12 18:08
__libc_start_main调用main 不应该是先把main的参数压栈 然后继续压lr和fp吗 所以main的参数应该在lr的上面吧?