创建空文件testpro.c
对testpro.c文件进行编辑
实验代码样例如下:
将参数改为和20242807相关的立即数
对修改完的c语言项目进行反汇编
查看反汇编生成的testpro项目汇编代码
如图所示,但是还有很多.***的编译器链接过程的辅助信息,这些不是本次研究重点
在vim页面输入(:g/\.s*/d)精简掉编译器在链接阶段的辅助信息后,可看到纯净的汇编代码
根据实验要求,生成x86架构的32位的汇编代码,其中第一个为g函数,这里逐步分析g函数在汇编语言中的每一步操作:
pushl %ebp
这是入栈操作,将当前栈帧基址寄存器 %ebp 的值压入栈中,保存旧的基址指针。
栈指针 %esp 会向下(即内存地址向低位)移动,以腾出空间,这个操作步骤是先移动栈指针,后按32位存入ebp寄存器。
movl %esp, %ebp
将栈指针 %esp 的值复制到基址寄存器 %ebp,建立新的栈帧。
注意这一步不是修改栈指针指向的地址空间。
这一操作用于为接下来的函数调用设置新的栈帧,方便访问局部变量和函数参数。
movl 8(%ebp), %eax
这是变址寻址的方式,目的在于将偏移量为 8 字节的值(即从 %ebp 基址寄存器所指向的地址加 8 字节偏移处的内容)移动到 %eax 寄存器中。
这里的 8(%ebp) 通常指的是函数的第一个参数。在 x86 汇编中,调用函数时,函数参数是通过栈传递的,而 %ebp + 8 是第一个参数的位置
addl $2024, %eax
这是一个立即寻址方式,将立即数 2024 加到 %eax 寄存器的当前值上。
这意味着给函数的参数(第一个参数)加上 2024,结果保存在 %eax 中。
popl %ebp
这一步是弹栈从栈中弹出保存的基址寄存器 %ebp 的值,将其恢复为旧栈帧的值。
这一步恢复进入该函数时的栈帧布局。
ret
返回指令,跳转到调用该函数的地方,并从栈中弹出返回地址,继续执行程序。
这一指令标志着函数执行完毕,返回给调用者。
函数f:
pushl %ebp
将当前的基址指针寄存器 %ebp 压入栈中,保存调用函数前的栈帧。
栈指针 %esp 向下移动(地址减小),为保存的 %ebp 腾出空间。
movl %esp, %ebp
将当前的栈指针 %esp 复制到基址寄存器 %ebp,创建新的栈帧。
这是常见的函数栈帧设置操作,用来管理局部变量和参数。
subl $4, %esp
栈指针 %esp 减少 4 个字节,给局部变量或传递给函数的参数腾出空间。
这里减去 4 个字节,通常用于为一个局部变量或函数参数在栈上分配空间。
movl 8(%ebp), %eax
将基址指针 %ebp 偏移 8 字节处的值移动到寄存器 %eax 中。
8(%ebp) 通常代表第一个函数参数,因为当调用一个函数时,第一个参数在栈中的位置是 %ebp + 8。
movl %eax, (%esp)
将 %eax 中的值存储到当前栈指针 %esp 指向的内存位置。
这一步是在为调用另一个函数 g 准备参数,将参数压入栈中供 g 使用。
call g
调用函数 g。这会将程序执行跳转到函数 g 的地址,同时把当前的返回地址(即函数调用后程序应该继续执行的位置)压入栈中。
g 会使用从栈中传递的参数进行计算。
leave
恢复旧的栈帧,等效于以下两条指令:
movl %ebp, %esp:将基址指针 %ebp 的值恢复到栈指针 %esp,丢弃当前函数栈帧的内容。
popl %ebp:从栈中弹出旧的 %ebp,恢复进入函数时保存的栈帧基址。
这意味着函数栈帧被销毁,栈指针和基址指针恢复到调用该函数前的状态。
ret
返回指令,恢复调用该函数前的执行状态,将程序跳回到调用函数 f 的位置。
代码的功能概述:
这个函数 f 的主要目的是从栈中读取第一个参数(偏移量为 %ebp + 8),然后将其作为参数传递给函数 g 并调用 g。
在调用 g 之前,它还为函数调用准备了 4 字节的空间用于传递参数。
函数 g 可能会对传递的参数进行处理,返回结果后,f 恢复栈帧并返回。
pushl %ebp
这行代码将当前的栈基指针(%ebp)压入栈中。它保存了调用函数时的栈基指针,用于在返回时恢复栈帧。
movl %esp, %ebp
将栈指针寄存器(%esp)的值复制到栈基指针寄存器(%ebp)中。这是设置当前函数的栈帧基指针。
subl $4, %esp
栈指针向下移动4个字节(减小%esp),为局部变量留出空间。
movl $28, (%esp)
将值28存储在当前栈顶(%esp)的位置。这是在为被调用函数f准备参数,假设f需要一个整数参数。
call f
调用函数f,此时处理器将当前指令地址压入栈,并跳转到函数f的地址执行。
addl $7, %eax
函数f返回后,返回值会存储在%eax中。这行代码将返回值加7,并将结果保存在%eax中。
leave
leave指令等价于:
mov %ebp, %esp:将栈指针恢复为函数调用之前的状态。
pop %ebp:恢复调用函数时保存的栈基指针。
ret
ret指令从栈中弹出返回地址,并跳转到该地址(返回到调用者处继续执行)
“从计算机是如何工作的”角度来看,从 C 语言文件到反汇编再到执行的过程可以分为几个关键步骤。下面是这一过程的详细分析:
1. 编写 C 语言代码
程序员使用 C 语言编写源代码(例如 program.c)。
源代码通常是人类可读的,包含逻辑和结构,用于实现某种功能。
2. 预处理(Preprocessing)
通过预处理器(如 cpp),C 编译器首先处理源代码中的指令,如 #include 和 #define。
预处理生成一个扩展的 C 文件(例如 program.i),此文件中包含了所有宏替换和头文件的内容。
3. 编译(Compilation)
预处理后的文件被传递给编译器,编译器将 C 代码转换为中间表示(通常是汇编语言)。
这个过程中,编译器会进行语法分析、语义分析、优化等,最终生成汇编代码(例如 program.s)。
4. 汇编(Assembly)
汇编器将汇编语言文件转换为机器代码,生成目标文件(例如 program.o)。
目标文件中包含了机器码和一些符号信息,但并不是一个完整的可执行文件。
5. 链接(Linking)
链接器将一个或多个目标文件与所需的库文件链接在一起,生成最终的可执行文件(例如 program)。链接过程包括符号解析、地址分配等,确保所有外部函数和变量都能正确引用。
6. 执行(Execution)
操作系统加载可执行文件到内存中,并为程序分配必要的资源。
CPU 从程序的入口点开始执行机器代码,执行过程中可能涉及:
上下文切换:在多任务环境中,操作系统管理不同程序的执行。
系统调用:程序请求操作系统提供的服务(如文件操作、网络通信等)。
内存管理:程序在运行中可能需要动态分配和释放内存。
7. 反汇编(Disassembly)
反汇编是将机器代码转换回汇编语言的过程。可以使用工具(如 objdump)来查看可执行文件的汇编代码。
反汇编的目的是帮助开发人员理解机器代码的执行流程或进行调试。
总结:本次实验是在学习了基本的Linux操作的基础上,通过对简单的C语言程序进行创建、编译、反汇编、链接和执行,确保程序能够高效且正确地运行在计算机上。其中这个实验主要集中于反汇编后汇编代码中各个寄存器对相应内存地址、数据进行操纵的方式。提供了从机器码回到可读的汇编代码的方式,帮助开发者分析程序的底层行为。反汇编后的汇编代码和各个寄存器与栈联系紧密,在汇编器将汇编代码转化为机器码后,CPU就能识别这些机器码,借助寄存器、内存等计算机组成部分,依次进行计算、数据传递、跳转等操作。
ARM64部分
由于实验平台不能保存,因此在ARM64的实验部分我选择在自己的Ubuntu虚拟机上运行
首先查看本机的默认编译环境:是64位x86的Linux系统,因此如果想要对ARM64架构进行反汇编,需要安装对应的交叉编译环境
如图,我安装了支持交叉编译的扩充包
和原先实验流程一样,但是这里是基于ARM64进行反汇编
查看汇编指令,这里详细汇编指令已经被简化:
对汇编指令进行分析:
g函数
sub sp, sp, #16: 为局部变量分配16字节的栈空间。
str w0, [sp, 12]: 将寄存器w0的值存储到栈中,偏移量为12字节。
ldr w0, [sp, 12]: 从栈中偏移12字节的位置加载值到w0。
add w0, w0, 2024: 将2024加到w0的值上。
add sp, sp, 16: 释放之前分配的栈空间。
ret: 返回到调用者。
f函数
stp x29, x30, [sp, -32]!: 将寄存器x29和x30的值保存到栈中,调整栈指针以留出32字节空间。
mov x29, sp: 更新帧指针x29为当前栈指针sp。
str w0, [sp, 28]: 将寄存器w0的值存储到栈中,偏移量为28字节。
ldr w0, [sp, 28]: 从栈中偏移28字节的位置加载值到w0。
bl g: 调用函数g。
ldp x29, x30, [sp], 32: 恢复x29和x30的值,从栈中弹出,并调整栈指针。
ret: 返回到调用者。
main函数
stp x29, x30, [sp, -16]!: 将寄存器x29和x30的值保存到栈中,调整栈指针以留出16字节空间。
mov x29, sp: 更新帧指针x29为当前栈指针sp。
mov w0, 28: 将28赋值给寄存器w0。
bl f: 调用函数f。
add w0, w0, 7: 将7加到w0的值上。
ldp x29, x30, [sp], 16: 恢复x29和x30的值,从栈中弹出,并调整栈指针。
ret: 返回到调用者。
“20242807孟宪聪 原创作品转载请注明出处 《Linux内核分析》MOOC课程Linux内核分析 - 网易云课堂 ”