前言
破解过程中经常会涉及到汇编代码的阅读和分析,因此理解汇编代码是入门逆向的敲门砖,本篇作为零基础入门逆向系列的第一篇简单总结了汇编语言中的代码结构。
汇编和逆向的关系
软件逆向工程的基本思路是将二进制代码按照一定格式进行正确有效的反汇编,并通过分析反汇编代码再配合其调用的外部函数或系统API等,对其代码逻辑进行理解,而在这个过程中最重要的就是推理出该二进制代码使用的数据结构。
变量作用域的识别
代码中声明的用于保存常量或字符串的东西叫变量。变量的范围可以是全局变量也可以是局部变量。
局部变量
局部变量保存的是函数内部的值,其作用域和生命周期局限于所在函数内。局部变量表示的是寄存器的偏移值。从汇编的角度的来看,局部变量分配空间时通常会使用栈和寄存器。
① 利用栈存放局部变量
局部变量在栈中进行分配,函数执行后会释放这些栈,程序用“sub esp, xxxx”为局部变量分配空间,用[ebp-xxxx]寻址调用局部变量。
初始化局部变量有两种方法:通过mov指令为变量赋值,例如“mov [ebp-], 5”;使用push指令直接将值压入栈,例如“push 5”。
② 利用寄存器存放局部变量
栈占用2个寄存器,编译器会利用剩下6个通用寄存器尽可能有效地存放局部变量。如果寄存器不够用,编译器就会将变量放到栈中。
全局变量
全局变量可以在代码逻辑的任何地方使用,放在全局变量的内存区中。常数一般放在全局变量中。
全局变量通常位于数据区块(.data)的一个固定地址处,当程序需要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址。一般编译器会将全局变量放到可读写的区块里。
可以利用全局变量来传递参数和函数返回值等。全局变量在程序的整个执行过程中占用内存单元。
静态局部变量
静态局部变量的数据保存方式及访问方式与全局变量基本一致,但是保存此变量的临近位置往往会有一个标志位控制其具体的作用域,当此标志位为1时证明此变量已经被初始化。
堆变量
堆变量的数据保存在new出来的堆空间中,其作用域由与new对应的delete控制,访问方式是使用new出来的指针访问其中的数据。
if-else分支
以常量为判断条件
Debug版的汇编代码特征如下:
Release版中,不可达的分支都由编译器在编译初期给剪掉了。
多次重复调用某个函数时,先将函数保存在寄存器中,然后在使用时直接调用寄存器。这样可以减少代码体积,而且CALL寄存器比CALL地址要快。
以变量为判断条件
Release版的汇编代码特征如下:
编译器还可能将重复的分支合并,这大大较少了编译器生成代码的体积。
识别三目运算符
① 有序常量的三目运算
setne(Set if Not Equal)指令根据ZF(Zero Flag)位的影响来决定是给al(也可以理解为eax)赋1还是0。
② 无序常量的三目运算
循环分支
代码逻辑中的循环语句是用来迭代一些操作,并且一直执行到某一个条件满足为止。
do-while循环
最大的特点是有条件判断的向上跳转。
while循环
for循环
for循环与while循环本质上是一样的,不同在于for循环在循环体内多了一个步长。
Debug版for循环的汇编特点:
switch-case分支
简单的switch-case分支识别技巧
复杂的switch-case分支识别
如果case的取值表示一个算术级数,那么编译器会利用一个跳转表(Jump Table)来实现。“jmp dword ptr[4*eax+xxxxxxxx]”指令相当于switch(a),根据eax的值进行索引,计算出指向相应case处理代码的指针。跳转表的作用就是用数组的寻址运算代替复杂的if-else分支,这可以大大提高程序的执行效率。
加减法的识别与优化原理
加法的优化:
减法的优化:
结尾
变量、if-else分支、循环分支、switch-case分支、逻辑运算,是学习一门计算机语言的基石,汇编也不例外,本文通过对这些简单的汇编逻辑进行总结,希望能够消除读者对汇编的恐惧感。
星期五实验室 出品
