参考链接:汇编语言简述(逆向工程)
汇编理解
寄存器
寄存器是CPU里的高速缓存区,为了提高效率,人们想到在CPU和内存之间加入高速缓存,提升效率
所以在编写汇编代码的时候,要先将数据写入寄存器中,CPU才能进一步曹祖偶
而寄存器又分为:
通用寄存器
指针和变址寄存器,指针寄存器又分为:基址指针寄存器、堆栈指针寄存器等
段寄存器
指令指针寄存器(PC)
标志寄存器
在做逆向分析过程中,关注最多的一般是通用寄存器、堆栈指针寄存器、标志寄存器
大端小端
内存中数据存储的顺序是需要我们注意的
计算机存储是以1个byte为单位存储的,但是很多数据要大于一个字节
大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
大端模式(网络字节序):
看到的就是 12 34 56 78,非常符合直觉。
小端模式(x86/x64 架构):
看起来反过来了,但 CPU 处理起来效率极高。
栈和堆
1️⃣ 抽象层面:数据结构上的“栈”
栈是一种“后进先出”(LIFO, Last In First Out)的线性表。
生活比喻:就像一摞盘子。你只能从最上面放盘子(压栈 Push),也只能从最上面拿盘子(出栈 Pop)。你想拿最底下的盘子,必须先拿走上面所有的盘子。
核心特征:
只允许在“栈顶”进行操作。
数据存取顺序是反的(最后进去的最先出来)。
在C语言里,函数的局部变量、参数传递,就是利用这种结构来管理的。
2️⃣ 内存层面:计算机里的“调用栈”(Call Stack)
函数调用的全过程(叠盘子和查字典)
假设我们在 main 函数里,调用了 func(10, 20)。此时栈是满的,我们只关注新增的动作。
第一步:传参(把食材扔进篮子)
程序会把参数从右到左(x86 cdecl约定)压入栈。
汇编动作:
push 20,push 10内存变化:栈顶(ESP)往下(低地址)移动,
10和20被放到了栈里。
第二步:保存返回地址(记住回来的路)
准备跳进 func 了,但跳进去之后怎么回来?CPU 自动把 main 里下一条指令的地址(返回地址)压入栈。
汇编动作:
call func内存变化:ESP 继续往下移,压入返回地址。
第三步:建立新基地(原 EBP 入栈 + 更新 EBP)
进入 func 内部,它要建立自己的地盘。
汇编动作:
push ebp(先把main的基地保存起来,将来好回家),然后mov ebp, esp(让 EBP 指向当前栈顶,作为func的新基地)。内存变化:此时,EBP 的位置就固定下来了。EBP 往上的地址(高地址)存着返回地址和参数;EBP 往下的地址(低地址)用来放
func自己的局部变量。
第四步:分配局部变量(占用空间)
汇编动作:
sub esp, 0x40(把栈顶往上拉 0x40 字节,相当于给func留出放局部变量的空位)。内存变化:ESP 往下走。你会发现,局部变量的地址比 EBP 小,所以在 IDA 里写成了
[rbp-8h];而参数比 EBP 大,写成[rbp+8h]或[rbp+10h]。 这就是为什么你在 IDA 里看到的是加号和减号!
第五步:执行函数体
函数开始干活。它想拿参数,就去 [ebp+8]、[ebp+12] 拿;想用局部变量,就去 [ebp-4]、[ebp-8] 拿。
第六步:退场(恢复现场)
函数干完活了,要回 main。
汇编动作:
leave(等价于mov esp, ebp; pop ebp。把 ESP 拉回 EBP 的位置,并且把之前保存的main的 EBP 恢复回去)。汇编动作:
ret(把之前压入的返回地址弹给 EIP,CPU 根据这个地址跳回main)。内存变化:栈里的垃圾数据被清理(其实就是 ESP 移动了),但物理内存里还留着残影,直到被下次压入覆盖。
栈
栈:大家都知道,栈是一种数据结构,数据按照先进后出的方式进行存储,汇编里也是如此,但一般只用于保存函数内的局部变量。在调用一个函数的时候,先预留足够数量的栈空间,供函数内部使用,在函数结束时,又需要移动栈指针空间,使之与进入函数前的地址保持一致,即所谓的栈平衡。
堆
堆:堆也是用于存放变量的,但不同于栈但是,栈用于保存申明方式定义的局部变量,而堆用于保存alloc或new方式申请的变量。由于是运行时才使用内存空间,就导致编译器无法知道你需要多大的空间,所以在C/C++里这种方式申请的内存需要自己手动释放。
我们写递归函数时,如果遇到死循环抛出异常,为什么叫StackOverflow异常?就是因为死循环调用函数时,会一直移动SP下移,最终栈内存会和堆内存相遇,这时候就是我们遇到的栈内存溢出异常了。
汇编语言的特点
没有申请、释放内存的概念,对内存操作没有限制,拿到内存地址就读写。不同于C/C++等语言里,局部变量的内存的分配和释放都是编译器在编译的时候完成的。
没有函数、方法等概念,高级语言中所有的函数、方法对应到汇编语言中都是一个个代码片段。而在汇编里实现高级语言的函数调用是:
保存当前代码地址的下一个指令地址
addr_a转跳到代码片段
fun_a的地址addr_b代码片段
fun_a执行完后,转跳到地址addr_a
x86_64寄存器
16个通用寄存器
常用指令
这里列举一些常见的指令,用于基本的逆向分析:
评论交流
欢迎留下你的想法