iOS逆向-从汇编代码理解函数调用栈

程序的栈空间有什么特点呢?首先会想到的就是,栈空间是往低地址增长的,当调用一个函数时,先开辟栈空间,用来存放当前函数的参数和局部变量;执行函数之前还需要先保护现场,当函数执行完之后会恢复现场。那么函数调用的过程中内存和CPU的寄存器到底发生了什么变化呢?这是本篇要探讨的问题。

先看一段C代码,定义了三个函数,调用关系为:funcA调用funcB, funcB调用funcC

int funcA(int a, int b) {
    int ret = funcB(a, b);
    return ret;
}

int funcB(int a, int b) {
    return funcC(a, b);
}

int funcC(int a, int b) {
    int c = a + b;
    return c;
}

从函数是否还调用了其他函数的角度看,函数可分为叶子函数非叶子函数
叶子函数:函数内部没有调用其他函数了,例如上面的funcC
非叶子函数:函数内部还调用了其他的函数,例如上面的funcA、funcB

为什么要这么划分呢?因为叶子函数非叶子函数生成的汇编代码有所不同,这也是本篇要分析的一个点。

获取汇编代码


得到对应的汇编代码有两种方式

方式一:
用clang编译器把C代码编译为汇编代码,编译函数所在的.c文件即可

xcrun -sdk iphoneos clang -S -arch arm64 Ctest.c

指定架构为arm64,执行命令后会得到Ctest.s文件,就是对应的汇编代码了。

方式二:
新建一个iOS项目,并且运行在真机设备上,在真机上才是arm64架构的汇编哦。在三个函数分别打断点,把Xcodedebug模式设置为显示汇编模式,在Debug -> Debug Workflow -> Aways Show Disassembly设置。当函数断住时,会显示当前函数的汇编代码。

这里采用方式二,得到的汇编代码如下:

funcA

 Assembly`funcA:
 0x100086ab8 <+0>:  sub    sp, sp, #0x20             ; =0x20
 0x100086abc <+4>:  stp    x29, x30, [sp, #0x10]
 0x100086ac0 <+8>:  add    x29, sp, #0x10            ; =0x10
 0x100086ac4 <+12>: stur   w0, [x29, #-0x4]
 0x100086ac8 <+16>: str    w1, [sp, #0x8]
 0x100086acc <+20>: ldur   w0, [x29, #-0x4]
 0x100086ad0 <+24>: ldr    w1, [sp, #0x8]
 0x100086ad4 <+28>: bl     0x100086aec               ; funcB at Ctest.c:57
 0x100086ad8 <+32>: str    w0, [sp, #0x4]
 0x100086adc <+36>: ldr    w0, [sp, #0x4]
 0x100086ae0 <+40>: ldp    x29, x30, [sp, #0x10]
 0x100086ae4 <+44>: add    sp, sp, #0x20             ; =0x20
 0x100086ae8 <+48>: ret

funcB

 Assembly`funcB:
 0x100086aec <+0>:  sub    sp, sp, #0x20             ; =0x20
 0x100086af0 <+4>:  stp    x29, x30, [sp, #0x10]
 0x100086af4 <+8>:  add    x29, sp, #0x10            ; =0x10
 0x100086af8 <+12>: stur   w0, [x29, #-0x4]
 0x100086afc <+16>: str    w1, [sp, #0x8]
 0x100086b00 <+20>: ldur   w0, [x29, #-0x4]
 0x100086b04 <+24>: ldr    w1, [sp, #0x8]
 0x100086b08 <+28>: bl     0x100086b18               ; funcC at Ctest.c:75
 0x100086b0c <+32>: ldp    x29, x30, [sp, #0x10]
 0x100086b10 <+36>: add    sp, sp, #0x20             ; =0x20
 0x100086b14 <+40>: ret

funcC

 Assembly`funcC:
 0x100086b18 <+0>:  sub    sp, sp, #0x10             ; =0x10
 0x100086b1c <+4>:  str    w0, [sp, #0xc]
 0x100086b20 <+8>:  str    w1, [sp, #0x8]
 0x100086b24 <+12>: ldr    w0, [sp, #0xc]
 0x100086b28 <+16>: ldr    w1, [sp, #0x8]
 0x100086b2c <+20>: add    w0, w0, w1
 0x100086b30 <+24>: str    w0, [sp, #0x4]
 0x100086b34 <+28>: ldr    w0, [sp, #0x4]
 0x100086b38 <+32>: add    sp, sp, #0x10             ; =0x10
 0x100086b3c <+36>: ret

汇编分析


先回忆一下几个关键寄存器和指令,
sp (Stack Point) :寄存器r31,指向函数调用栈的栈顶
fp (Frame Point):寄存器r29,指向当前正在执行函数栈帧的栈底
lr (Link Register) :寄存器r30,存储的是函数返回地址,用于当函数结束时,返回函数调用方继续往下执行。
bl:跳转指令,它做了两件事情,
1. 把下一条指令的地址存储到lr寄存器中
2. 跳转到标记处执行指令
ret:函数返回,返回到lr保存的地址继续执行指令。
更多的寄存器和指令学习可参考iOS逆向-arm64汇编学习

由于funcAfuncB都是非叶子函数,生成的主要汇编代码大致相同,所以下面只分析funcBfuncC对应的汇编代码。

Assembly funcB:

 Assembly`funcB:
// 第一部分:开辟栈空间,保护现场
 0x100086aec <+0>:  sub    sp, sp, #0x20           ;// sp指针往下移动0x20个字节,开辟栈空间。
 0x100086af0 <+4>:  stp    x29, x30, [sp, #0x10]   ;// x29(fp)、x30(lr)寄存器中的内容存放内存中
 0x100086af4 <+8>:  add    x29, sp, #0x10          ;// x29(fp) 栈底指针往下移动0x10个字节。

// 第二部分:函数逻辑
 0x100086af8 <+12>: stur   w0, [x29, #-0x4]        ;// 把第一个参数存入内存中
 0x100086afc <+16>: str    w1, [sp, #0x8]          ;// 把第二个参数存入内存中
 0x100086b00 <+20>: ldur   w0, [x29, #-0x4]        ;// 从内存中读取到w0中,也就是第一个参数
 0x100086b04 <+24>: ldr    w1, [sp, #0x8]          ;// 从内存中读取到w1中,也就是第二个参数
 0x100086b08 <+28>: bl     0x100086b18             ;// 调用 funC 函数

// 第三部分:恢复现场,回收栈空间
 0x100086b0c <+32>: ldp    x29, x30, [sp, #0x10]   ;// 从内存中读取数据到x29、x30,恢复 x29、x30的值。
 0x100086b10 <+36>: add    sp, sp, #0x20           ;// 函数执行完毕,回收栈空间
 0x100086b14 <+40>: ret                            ;// 返回,跳转回上一个函数(funcA)执行

Assembly funcC

 Assembly`funcC:
// 第一部分:开辟栈空间
 0x100086b18 <+0>:  sub    sp, sp, #0x10             ;// sp指针往下移动0x20个字节,开辟栈空间。

// 第二部分:函数逻辑
 0x100086b1c <+4>:  str    w0, [sp, #0xc]
 0x100086b20 <+8>:  str    w1, [sp, #0x8]
 0x100086b24 <+12>: ldr    w0, [sp, #0xc]
 0x100086b28 <+16>: ldr    w1, [sp, #0x8]
 0x100086b2c <+20>: add    w0, w0, w1
 0x100086b30 <+24>: str    w0, [sp, #0x4]
 0x100086b34 <+28>: ldr    w0, [sp, #0x4]

// 第三部分:回收栈空间
 0x100086b38 <+32>: add    sp, sp, #0x10             ;// 函数执行完毕,回收栈空间
 0x100086b3c <+36>: ret                              ;// 返回,跳转回上一个函数(funcB)执行

经过上面的分析,Assembly funcB:Assembly funcC:最大的却别是在Assembly funcB:中有对x29 (fp)、x30 (lr)的内容存储到内存,进行保护,并在结束时恢复其原来的值;而在Assembly funcC:中没有这样的操作。

原因分析:

  1. Assembly funcC:bl指令,bl指令会修改x30( lr )寄存器的值,所以需要在真正执行函数之前保存。
  2. 那为什么要保存x29的值呢?从汇编代码看,x29是被修改了,但是不修改也可以的,其他指令并没有修改x29的值;单独通过sp也可以完成所有的寻址(访问具体的内存空间)工作。说说我个人的理解
    2.1)通过x29 (fp)x30(sp)可以确定当前函数的栈帧的地址范围,寻址时不可以超出这个范围。访问超出这个范围地址的数据可能是脏数据,至少对当前函数来说是脏数据。
    2.2)有了x29寄存器,寻址的时候可以用x29的值做一个偏移定位到要访问的地址,也可以用sp的值做一个偏移找到要访问的地址。靠近栈底的用x29做偏移,靠近栈顶的用sp做偏移,这样可以提高寻址的速度。那CPU怎么知道要访问的地址是靠近x29还是靠近sp呢?CPU是不知道的,但编译器知道,要访问什么地址,在程序编译的时候就确定了。
    2.3)那Assembly funcC:为什么没有栈底指针x29,有的话也可以提高寻址速度啊。那为什么没有自己的栈底指针呢?目前还没找到答案,也是自己的一个疑惑。

好了,上面啰嗦了那么多,下面通过图来看函数调用时栈空间寄存器的变化情况,

stack.png

对图稍微说明一下,栈是高地址往低地址增长的,图中每一行表示4个字节。左边是函数调用时spfp的变化过程,右边是函数调用结束后spfp的变化过程。通过观察,开辟栈空间时,不是需要多大的内存就开闭多少的内存空间,而是16的倍数的字节数。这要做有助于提高CPU访问内存的速度。

总结


通过上面的分析,理解了函数调用时栈空间寄存器的变化情况,以及为什么需要那样变化。但还留下了一个疑问,叶子函数为什么没有自己fp,知道的大神麻烦告知。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容