《C++反汇编与逆向分析技术揭秘》笔记(1)

对于《C++反汇编与逆向分析技术揭秘》第一章~第四章的学习记录。

第一章 熟悉工作环境和相关工具

1 调试工具Microsoft Visual C++ 6.0和OllyDBG

  1. OllyDBG的基本快捷键

2 反汇编静态分析工具IDA
  1. IDA的常用快捷键

  1. IDA的各视图窗口说明

  1. IDA函数名称识别

(1)SIG文件的制作步骤;

(2)将生成后的SIG文件放置在IDA的安装目录SIG文件夹下,使用快捷键Shift+F5添加SIG文件到分析工程中。

(3)SIG文件制作批处理文件

3 反汇编引擎的工作原理

第二章 基本数据类型的表现形式

1 整数类型

  1. 无符号整数

  1. 有符号整数

2 浮点数类型
  1. 浮点数的编码方式(float、double、IEEE编码)

  1. 基本的浮点数指令

3 字符和字符串

  1. 字符的编码(ASCII和Unicode)

  1. 字符串的存储方式

4 布尔类型

C++中定义0为假,非0为真。

5 地址、指针和引用
  1. 指针

由于指针保存的数据都是地址,所以无论什么类型的指针占据的内存空间大小都是一样的(32位-4字节;64位-8字节)。

  1. 指针和地址的区别


  1. 各类型指针的工作方式
  • 指针的取内容操作分为两个步骤:先取出指针中保存的地址信息,然后针对这个地址进行取内容,也就是一个间接寻址的过程,这也是识别指针的重要依据;
  • 在C++中,所有指针类型只支持加法和减法。指针是用于保存数据地址、解释地址而存在的。因此,只有加法与减法才有意义,其他运算对于指针而言没有任何意义。
  • 指针加法用于地址偏移,但指针的加法并不像数学中的加法那样简单。指针加1后,指针内保存的地址值并不一定会加1,具体的值取决于指针类型,如指针类型为int,地址值将会加4。这个4是根据类型大小所得到的值。
  • C++为什么要用这种烦琐的地址偏移方法呢?当指针中保存的地址为数组首地址时,为了能够利用指针加1后访问到数组内下一成员,所以加的是类型长度,而非数字1。
  • 指针可以做减法操作,但乘法与除法对于指针寻址而言是没有意义的。
  • 两指针做减法操作是在计算的两个地址之间的元素个数,结果为有符号整数,进行减法操作的两指针必须是同类指针相减。可用于两指针中的地址比较,也可用于其他场合,比如求数组元素个数。其计算公式如下——

  • 另外,两指针相加也是没有意义的。
  1. 引用


6 常量

  1. 常量的定义

  1. #define和const的区别

7 总结


第三章 认识启动函数,找到用户入口

1 main函数的识别

2 总结

第四章 观察各种表达式的求值过程

1 算术运算和赋值
  • 算术运算是指加法、减法、乘法、除法这四种数学运算,也称为四则运算;
  • 赋值运算类似于数学中的“等于”,是将一个内存空间中的数据传递到另一个内存空间中。由于内存没有处理器那样的控制能力,各个内存单元之间是无法直接传递数据的,必须通过处理器访问并中转,以实现两个内存单元间的数据传输。
  • 在VC++6.0中,算术运算与其他传递计算结果的代码组合后才能被视为一条有效的语句,如赋值运算或函数的参数传递。单独的算术运算虽然可以编译通过,但是并不会生成代码。因为只进行计算而没有传递结果的运算不会对程序结果有任何影响,此时编译器将其视为无效语句,与空语句等价,不会有任何编译处理。
  1. 各种算术运算的工作形式

(1)加法

在编译过程中,编译器常常会采用“常量传播”和“常量折叠”这样的方案对代码中的变量与常量进行优化。

  • 常量传播:将编译期间可计算出结果的变量转换成常量,这样就减少了变量的使用;
  • 常量折叠:当计算公式中出现多个常量进行计算的情况时,且编译器可以在编译期间计算出结果时,这样源码中所有的常量计算都将被计算结果代替。

如下示例——

void main(){
    int nvar=1+5-3*6;
    printf("nvar = %d \r\n", nvar);
}

优化之后就会变成——

void main(){
    printf("nvar = %d \r\n", -12);
}

使用常量的好处是可以生成立即数寻址的目标代码,常量作为立即数成为指令的一部分,从而减少了内存的访问次数。

(2)减法

在实际分析中,根据加法操作数的情况,当加数为负数时,执行的并非是加法而是减法操作。

(3)乘法

(4)除法

  • 除法计算约定

除法运算对应的汇编指令分有符号idiv和无符号div两种。除法指令的执行周期较长,效率也较低,所以编译器想尽办法用其他运算指令代替除法指令。C++中的除法和数学中的除法不同。在C++中,除法运算不保留余数,有专门求取余数的运算(运算符为%),也称之为取模运算。对于整数除法,C++的规则是仅仅保留整数部分,小数部分完全舍弃。

对于除法而言,计算机面临着如何处理小数部分的问题。在数学意义上,7/2=3.5,而对于计算机而言,整数除法的结果必须为整数。对于3.5这样的数值,计算机取整数部分的方式有如下几种:向下取整(向下取整的除法,当除数为2的幂时,可以直接用带符号右移指令(sar)来完成)、向上取整、向零取整。

在c语言和其他多数高级语言中,对整数除法规定为向零取整,也即是放弃小数部分(“截断除法”)。

  • VC++6.0对整数除法的优化和论证

如果除数是变量,则只能使用除法指令。如果除数为常量,就有了优化的余地。根据除数值的相关特性,编译器有对应的处理方式。详细的代码示例及优化策略见博客《汇编与对应C++伪码》中对应章节部分的内容。

(5)取模

  1. 算术结果的溢出

2 关系运算和逻辑运算
  1. 条件跳转指令表

  1. 条件表达式

3 位运算

对于左移运算而言,无符号数和有符号数的移位操作是一样的,都不需要考虑到符号位。但右移运算则有变化,有符号数对应的指令为sar,可以保留符号位;无符号数不需要符号位,所以直接使用shr将最高位补0。故我们可以通过shr和sar指令判断操作数是否有符号。

4 编译器的优化技巧

代码优化,是指为了达到某一种优化目的对程序代码进行变换。这样的变换有一个原则:变换前和变换后等价(不改变程序的运行结果)。就优化目的而论,代码优化一般有四个方向:

  • 执行速度优化;
  • 内存存储空间优化;
  • 磁盘存储空间优化;
  • 编译时间优化(别诧异,大型软件编译一次需要好几个小时是常事);

如今,计算机的存储空间都不小,因此常见的优化都是以执行速度的优化为主,这里也仅以速度优化为主展开讨论。编译器的工作过程中可以分为几个阶段:预处理→词法分析→语法分析→语义分析→中间代码生成→目标代码生成。其中,优化的机会一般存在于中间代码生成和目标代码生成这两个阶段。尤其是在中间代码生成阶段所做的优化,这类优化不具备设备相关性,在不同的硬件环境中都能通用,因此编译器设计者广泛采用这类办法。

常见的与设备无关(中间代码生成阶段)的优化方案有以下几种——

  • 常量折叠;
  • 常量传播;
  • 减少变量;
  • 公共表达式;
  • 复写传播;
  • 减去不可达分支(减支优化);
  • 顺序语句代替分支;
  • 强度削弱;
  • 数学变换;
  • 代码外提;

目标代码生成阶段(与设备有关)的优化方案有以下几种——

  • 流水线优化;
  • 分支优化;
  • 高速缓存优化;