20252803-反汇编(分析调用机制,调用栈的形成和释放代码,使用gdb跟踪汇编代码)

任务1: 实践上面内容

任务2 :基于香橙派

你好,滕森!今天我要带你深入了解ARM64的世界。作为你的专属讲师,我会根据你的中等水平来调整讲解深度——既不会太简单让你觉得无聊,也不会太复杂让你跟不上。我们将在40分钟内完成这个充满挑战又收获满满的学习旅程!

ARM 历史和体系结构介绍

让我先从一个故事开始:想象一下,1985年英国剑桥有一家小公司叫Acorn,他们需要为自家电脑设计更省电的处理器。就像发现普通汽车太耗油,于是决定自己造一辆节能车一样,他们创造了ARM架构。

关键突破在于ARM选择了与其他处理器完全不同的设计哲学:精简指令集(RISC)。这就像是你整理行李箱时,不带整个衣柜,而是精选几件多功能衣服——虽然每件衣服功能单一,但组合起来效率极高!

对比一下:Intel的x86架构像是一把瑞士军刀,功能复杂但耗电;而ARM就像专业厨师的一套刀具,每把刀专注一个任务,配合使用效率惊人。正是这个设计,让ARM最终统治了移动设备市场——你的手机、平板都在用ARM芯片!

ARM64 汇编语言介绍

现在我们来谈谈汇编语言。如果把编程比作做菜,高级语言(如Python)就像使用预制菜,简单但不够精细;而汇编语言就像是亲自挑选食材、掌握火候——虽然复杂,但能做出最地道的味道!

ARM64汇编的特殊之处在于它的指令都非常规整。比如数据处理指令大多都是"操作 目标寄存器,源寄存器1,源寄存器2"这样的三地址格式。这就像做菜时总遵循"主料+配料+调料"的固定步骤,特别容易记忆和掌握。

我建议你这样理解:把寄存器想象成厨房里的工作台(共31个,编号x0-x30),每个工作台只能临时放一个食材。而内存就像是冰箱,可以存储大量食材但存取速度较慢。

ARM64 汇编语言极简例子的编译,运行过程

让我们看一个最简单的例子,就像学做菜先从煎蛋开始:

// 简单加法程序
add x0, x1, x2    // 把x1和x2工作台的数相加,结果放回x0工作台

编译运行过程分为四个关键阶段:

1、写源码 - 就像写菜谱
2、汇编器翻译 - 把菜谱翻译成厨房能理解的指令
3、链接器整合 - 把多个菜谱组合成完整宴席菜单
4、加载执行 - 厨师按照菜单开始做菜
这个过程中最容易被忽略的是链接阶段——就像请客时需要把凉菜、热菜、汤品的制作顺序合理安排,否则厨房会乱套!

ARM64 指令集,寻址方式,顺序分支,循环等结构,C语言与汇编的互相调用

现在我们来学习ARM64的"烹饪技法大全":

寻址方式就像取食材的不同方法:

立即数寻址:直接从调料罐取盐(add x0, x1, #5)
寄存器寻址:从另一个工作台拿半成品(add x0, x1, x2)
内存寻址:需要去冰箱取食材(ldr x0, [x1])

控制结构是烹饪的流程控制:

顺序结构:按步骤炒菜→放盐→出锅
分支结构:如果太咸就加水,否则直接装盘
循环结构:搅拌直到均匀为止

最实用的技能是C与汇编的混合编程。这就像中餐厨师偶尔用西餐酱料——保持主体风格的同时吸收精华。你可以在C代码中关键部分插入汇编,获得极致性能!

记住这个认知框架:ARM64汇编 = 工作台(寄存器) + 食材搬运(寻址) + 烹饪操作(指令) + 流程控制(分支循环)

学完这些,你不仅能理解手机为什么这么省电,还能在需要极致性能的场合(如嵌入式开发、游戏引擎)大显身手。现在你可以尝试在香橙派上写第一个汇编程序了——从简单的LED控制开始,慢慢挑战更复杂的任务!

1 、通过输入gcc -S -o main.s main.c 将下面c程序”week0401学号.c“编译成汇编代码,分析调用机制,调用栈的形成和释放代码

int g(int x){
return x+3;
}
int f(int x){
int i = 03;
return g(x)+i;
}
int main(void){
return f(8)+1;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用cat命令获取main.s中的汇编代码,已加上注释:

        .arch armv8-a                  ; 基于ARMv8架构
        .file   "test.c"               ; 对应C源文件
        .text                          ; 代码段开始
        .align  2
        .global g                      ; 声明g为全局函数
        .type   g, %function
g:                                    ; g函数入口
.LFB0:
        .cfi_startproc                 ; CFI(调用帧信息)开始
        sub     sp, sp, #16            ; 1. 栈帧形成:sp = sp - 16(分配16字节栈空间)
        .cfi_def_cfa_offset 16         ; CFI:当前栈顶偏移16字节
        str     w0, [sp, 12]           ; 2. 保存参数:将w0(x的值)存入栈偏移12处
        ldr     w0, [sp, 12]           ; 3. 取参数x:从栈偏移12处加载x到w0
        add     w0, w0, 3              ; 4. 计算返回值:w0 = x + 3(返回值存入w0)
        add     sp, sp, 16             ; 5. 栈帧释放:sp = sp + 16(回收16字节栈空间)
        .cfi_def_cfa_offset 0          ; CFI:栈顶偏移恢复为0
        ret                            ; 6. 返回:从x30取返回地址,跳转回f
        .cfi_endproc                   ; CFI结束
.LFE0:
        .size   g, .-g                 ; g函数大小定义

        .align  2
        .global f                      ; 声明f为全局函数
        .type   f, %function
f:                                    ; f函数入口
.LFB1:
        .cfi_startproc                 ; CFI开始
        stp     x29, x30, [sp, -48]!   ; 1. 栈帧形成:
                                       ;    - 预递减sp:sp = sp - 48(分配48字节栈空间)
                                       ;    - 保存x29(调用者main的帧指针)和x30(返回到main的地址)到栈顶
        .cfi_def_cfa_offset 48         ; CFI:当前栈顶偏移48字节
        .cfi_offset 29, -48            ; CFI:x29(main的帧指针)保存在栈偏移-48处
        .cfi_offset 30, -40            ; CFI:x30(返回地址)保存在栈偏移-40处
        mov     x29, sp                ; 2. 建立当前帧指针:x29(FP)= sp(栈顶)
        str     w0, [sp, 28]           ; 3. 保存参数:将w0(x的值)存入栈偏移28处
        mov     w0, 3                  ; 4. 赋值局部变量i:w0 = 3(学号后两位03)
        str     w0, [sp, 44]           ;    存入栈偏移44处(i的存储位置)
        ldr     w0, [sp, 28]           ; 5. 准备调用g:从栈偏移28处取x到w0(作为g的参数)
        bl      g                      ; 6. 调用g:
                                       ;    - 将下一条指令地址存入x30(返回到f的地址)
                                       ;    - 跳转至g函数
        mov     w1, w0                 ; 7. 暂存g的返回值:w1 = g(x)的结果
        ldr     w0, [sp, 44]           ; 8. 取局部变量i:从栈偏移44处加载i到w0
        add     w0, w1, w0             ; 9. 计算返回值:w0 = g(x) + i(结果存入w0)
        ldp     x29, x30, [sp], 48     ; 10. 栈帧释放:
                                       ;     - 从栈中恢复x29(main的帧指针)和x30(返回地址)
                                       ;     - 后递增sp:sp = sp + 48(回收48字节栈空间)
        .cfi_restore 30                ; CFI:恢复x30
        .cfi_restore 29                ; CFI:恢复x29
        .cfi_def_cfa_offset 0          ; CFI:栈顶偏移恢复为0
        ret                            ; 11. 返回:从x30取地址,跳转回main
        .cfi_endproc                   ; CFI结束
.LFE1:
        .size   f, .-f                 ; f函数大小定义

        .align  2
        .global main                   ; 声明main为全局函数
        .type   main, %function
main:                                  ; main函数入口
.LFB2:
        .cfi_startproc                 ; CFI开始
        stp     x29, x30, [sp, -16]!   ; 1. 栈帧形成:
                                       ;    - 预递减sp:sp = sp - 16(分配16字节栈空间)
                                       ;    - 保存x29(调用者的帧指针)和x30(返回地址)到栈顶
        .cfi_def_cfa_offset 16         ; CFI:当前栈顶偏移16字节
        .cfi_offset 29, -16            ; CFI:x29保存在栈偏移-16处
        .cfi_offset 30, -8             ; CFI:x30保存在栈偏移-8处
        mov     x29, sp                ; 2. 建立当前帧指针:x29(FP)= sp(栈顶)
        mov     w0, 8                  ; 3. 准备调用f:w0 = 8(作为f的参数x)
        bl      f                      ; 4. 调用f:
                                       ;    - 将下一条指令地址存入x30(返回到main的地址)
                                       ;    - 跳转至f函数
        add     w0, w0, 1              ; 5. 计算返回值:w0 = f(8)的结果 + 1
        ldp     x29, x30, [sp], 16     ; 6. 栈帧释放:
                                       ;    - 从栈中恢复x29和x30
                                       ;    - 后递增sp:sp = sp + 16(回收16字节栈空间)
        .cfi_restore 30                ; CFI:恢复x30
        .cfi_restore 29                ; CFI:恢复x29
        .cfi_def_cfa_offset 0          ; CFI:栈顶偏移恢复为0
        ret                            ; 7. 返回:从x30取地址,结束程序
        .cfi_endproc                   ; CFI结束
.LFE2:
        .size   main, .-main           ; main函数大小定义
        .ident  "GCC: (GNU) 10.3.1"    ; 编译器标识
        .section        .note.GNU-stack,"",@progbits  ; 栈属性标记

调用机制:

在 ARMv8 架构下,函数调用机制基于特定的寄存器约定和指令流程。其中,x29(FP)作为帧指针,用于标记当前栈帧的基地址,方便定位栈内数据;x30(LR)为链接寄存器,专门存储函数的返回地址,在执行bl指令调用函数时,会自动将下一条指令的地址写入该寄存器;sp作为栈指针,始终指向栈顶,且 ARM 架构中栈向低地址增长。参数传递和返回值通过w0-w7这 8 个 32 位寄存器完成,前 8 个参数依次存入这些寄存器,函数返回值则通过w0传递。核心指令中,stp x29, x30, [sp, -n]!用于预递减栈指针,分配n字节的栈空间,同时将调用者的x29和x30保存到栈顶,以此建立新的栈帧;ldp x29, x30, [sp], n则用于后递增栈指针,释放n字节的栈空间,并从栈中恢复x29和x30,实现当前栈帧的销毁;bl 函数名负责跳转至目标函数,并记录返回地址;ret指令则从x30中读取返回地址,跳转回调用者。

调用栈的形成:

调用栈的形成过程随函数调用逐层展开。在main函数中,首先执行stp x29, x30, [sp, -16]!,分配 16 字节栈空间,将其调用者的x29和x30存入栈中,再通过mov x29, sp将当前栈指针设为main的帧指针,完成自身栈帧的建立。随后,main将参数 8 存入w0,通过bl f调用f函数,此时x30被设置为main中bl f之后指令的地址,作为从f返回的入口。进入f函数后,执行stp x29, x30, [sp, -48]!,分配 48 字节栈空间,保存main的x29和x30(即返回main的地址),再用mov x29, sp建立f的帧指针。f将接收的参数x存入栈偏移 28 处,将局部变量i=3存入栈偏移 44 处,之后从栈中取出x存入w0,通过bl g调用g函数,x30此时记录f中bl g之后指令的地址。进入g函数后,执行sub sp, sp, #16分配 16 字节栈空间,将参数x存入栈偏移 12 处,完成g栈帧的建立。

释放代码:

调用栈的释放则按函数返回顺序逐层进行。g函数计算完成后,执行add sp, sp, #16,将栈指针回移 16 字节,释放自身栈空间,随后ret指令从x30取出返回地址,跳转回f函数。f函数在接收g的返回值后,计算g(x)+i的结果并存入w0,接着执行ldp x29, x30, [sp], 48,将栈指针回移 48 字节释放自身栈空间,同时恢复main的x29和x30,最后ret指令跳转回main函数。main函数接收f的返回值后,计算f(8)+1的结果并存入w0,执行ldp x29, x30, [sp], 16,栈指针回移 16 字节释放自身栈空间,恢复其调用者的x29和x30,最终ret指令结束程序,完成整个调用栈的释放。

2、参考http://www.cnblogs.com/lxm20145215----/p/5982554.html,使用gdb跟踪汇编代码,在纸上画出f中每一条语句引起的主要的寄存器(PC,堆栈指针等)的值和栈的变化情况。提交截图。

对于上面的main.c,编译带调试信息的可执行文件

 gcc -g main.c -o main  # 生成可执行文件main(带调试信息)

使用 gdb 跟踪f函数的汇编执行

启动 gdb 并设置断点:

gdb ./main
(gdb) b f  # 在f函数入口处设置断点
(gdb) r    # 运行程序,停在f函数开始处

在这里插入图片描述

进入之后先在main函数处设置一个断点,再run一下,使用disassemble指令获取汇编代码:

在这里插入图片描述

stp     x29, x30, [sp, #-16]!  ; 1. 预递减栈指针16字节(分配栈空间),同时将当前x29(调用者帧指针)和x30(返回地址)存入栈顶,用于保存调用者的栈帧状态
mov     x29, sp               ; 2. 将当前栈指针sp的值赋给x29,建立当前函数(main)的帧指针,确定当前栈帧的基地址
mov     w0, #0x8              ; 3. 将立即数8存入w0寄存器,作为调用函数f的参数(对应C代码中f(8)的实参8)
bl      0x40061c <f>          ; 4. 调用函数f:将下一条指令(add w0, w0, #0x1)的地址存入x30(链接寄存器),然后跳转到f函数的入口地址0x40061c
add     w0, w0, #0x1          ; 5. 从函数f返回后,将f的返回值(存于w0)加1,结果存入w0(作为main函数的返回值)
ldp     x29, x30, [sp], #16   ; 6. 从栈中恢复之前保存的x29(调用者帧指针)和x30(返回地址),同时栈指针sp后递增16字节(释放栈空间),恢复调用者的栈帧状态
ret                           ; 7. 函数返回:从x30寄存器取出返回地址,跳转到该地址,结束main函数的执行

用i(info) r(registers)指令查看各寄存器的值

在这里插入图片描述
结合display命令和寄存器或pc内部变量,做如下设置:display /i $pc,这样在每次执行下一条汇编语句
时,都会显示出当前执行的语句。下面展示每一步时%esp、%ebp和堆栈内容的变化:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值