一、前言

本文以一个简单的例子来描述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

转载于:https://www.cnblogs.com/alantu2018/p/8457648.html

计算机科学基础知识(六)理解栈帧相关推荐

  1. 学计算机基础代码,计算机科学基础知识(示例代码)

    1. 计算机科学基础知识 1.1 数制及其转换 二进制.八进制.十进制和十六进制等常用数制及其相互转换 1.2 计算机内数据的表示 数的表示(原码.反码.补码.移码表示,整数和实数的表示,精度和溢出) ...

  2. Unity基础知识学习七,帧同步源码学习

    前言 在学习帧同步框架源码之前,先过一遍基础知识:Unity基础学习六,网络同步_u013617851的博客-CSDN博客 视频地址:帧同步教程[合集]_哔哩哔哩_bilibili github地址: ...

  3. 计算机科学基础知识(四): 动态库和位置无关代码

    一.前言 本文主要描述了动态库以及和动态库有紧密联系的位置无关代码的相关资讯.首先介绍了动态库和位置无关代码的源由,了解这些背景知识有助于理解和学习动态库.随后,我们通过加-fPIC和不加这个编译选项 ...

  4. 嵌入式Linux应用开发基础知识(六)——Makefile实例

    前面我们学了很多Makefile相关的知识,但是没有写过一个完整的代码,这一章我们写出一个实例 一.完善Makefile 在之前我们写了一个较为完善的Makefile程序,但是还是存在一些问题,我们需 ...

  5. 音视频基础知识——素材理解

    素材 素材是媒体内容生产中一切生产资料的集合,包括不限于视频.音频.图片.字幕等形式. 素材通过统一的协议把原始的数据有序组织起来,便于编辑与管理.比如一般的素材是由一个物理文件及其各类属性构成,在对 ...

  6. FPGA基础知识之理解LUT

    LUT指显示查找表(Look-Up-Table),本质上就是一个RAM.它把数据事先写入RAM后,每当输入一个信号就等于输入一个地址进行查表,找出地址对应的内容,然后输出. 第一部分: 查找表LUT ...

  7. 前端基础知识:理解 Web Worker

    保持应用程序的流畅(smooth)和灵敏(responsive)是具有好的性能的应用程序的目标.按照 RAIL 模型,灵敏意味着响应用户行为的时间控制在 100ms 内,而流畅意味着屏幕上任何元素移动 ...

  8. 计算机六年级基础知识,六年级计算机试题

    六年级计算机试题 一.多音字组词. 二.写出下列词语的近义词. 诞生--(  )   信息--(  )   获取--(  ) 处理--(  )   传递--(  )   预料--(  ) 三.把不能搭 ...

  9. 6.java中什么是类_类、对象(java基础知识六)

    1.Java约定俗成 java约定俗成1,类名接口名 一个单词首字母大写,多个单词每个单词首字母都大写2,方法名和变量名 一个单词全部小写,多个单词从第二个单词首字母大写 建议:如果能用英语尽量用英语 ...

最新文章

  1. 卫星发现,这里用十年逆转了千年!
  2. PAT甲级1070 Mooncake:[C++题解]贪心
  3. 面试准备系列01----面试中的链表题目汇总
  4. SlidingMenu的简单使用
  5. php定时执行任务没有执行,linux中定时任务crontab中的php任务无法执行,求可能的原因...
  6. 实例61:python
  7. python selenium 下载文件_Python Selenium —— 文件上传、下载,其实很简单
  8. 开源远程访问服务器工具_为什么开源需要可访问性标准
  9. Web前端 Javascript笔记(1)数组
  10. Java开发之I/O读取文件实例详解
  11. 一个完整的机器学习模型的流程
  12. html5开发app的视频教程及相关资料
  13. 数学建模与数据分析中的主成分分析
  14. JQuery常见命令查找网站
  15. Python Factory 工厂方法
  16. 他向导师下跪,仍被强制退学!5年博士白读,双方各执一词,同门师兄也有回应……...
  17. 百度智能云BCC云服务器释放实例
  18. grasscutter 使用指南——Android/Windows/IOS端均已支持
  19. 《道德经》的三个重要版本
  20. 新一代的 Python 包管理工具 -- PDM

热门文章

  1. 无需「域外」文本,微软:NLP就应该针对性预训练
  2. 强化学习样本复杂性综述
  3. SAP S4HANA精华帖集锦
  4. 如何评价算法的好坏?
  5. 「图像分割模型」编解码结构SegNet
  6. AI综述专栏 | 基于深度学习的目标检测算法综述
  7. TensorFlow基本使用
  8. 如何使用机器学习进行异常检测和状态监控?
  9. SAP MM 工序委外流程初探
  10. 业界丨一文看懂AI人才百万美元年薪因何而来?