文章目录
#基本数据类型
反汇编一个基本的知识点就是掌握数据类型,包括整形和浮点类型在内存中是如何存放的,这里要知道原码,反码,补码,以及IEEE浮点标准,这部分与处理器架构无关,《C++反汇编与逆向分析技术揭秘》 第二章已经有详细讲解,这里略过。
在此推荐下我曾经写过的一个 进制转换的工具 来学习数据类型 。
Cpp中的引用
C++为了简化指针操作,对其进行了封装,产生了引用类型。实际上引用类型就是指针类型,只不过用于存放地址的内存空间对使用者而言是隐藏的。下面看一段代码:
#include <iostream>
using namespace std;
void Add(int &nVar)
{
nVar ++;
}
int main()
{
int nVar = 0x12345678;
int &nVarType = nVar;
Add(nVar);
cout << &nVarType << endl;
cout << &nVar << endl;
cout << hex << nVar << endl;
nVarType ++;
cout << hex << nVar << endl;
}
编译$ arm-linux-c++ -static 1.c
之后放到上一节的qemu-arm虚拟机中跑下运行结果如下:
用 arm-linux-objdump -D -m arm
看下反汇编结果:
000091fc <_Z3AddRi>:
91fc: e52db004 push {fp} ; (str fp, [sp, #-4]!)
;这里来到子函数add,首先push fp保存栈帧,相当于x86下的EBP寄存器
9200: e28db000 add fp, sp, #0
9204: e24dd00c sub sp, sp, #12 ;开辟一个栈帧
9208: e50b0008 str r0, [fp, #-8]
920c: e51b3008 ldr r3, [fp, #-8] ;生成了好多冗余指令
9210: e5933000 ldr r3, [r3] ;这里是得到 nVar 数值
9214: e2832001 add r2, r3, #1 ;这里不用说就是 ++
9218: e51b3008 ldr r3, [fp, #-8] ;得到nVar 地址
921c: e5832000 str r2, [r3] ;把自加之后的值给 nVar
9220: e24bd000 sub sp, fp, #0 ;后面两条指令恢复栈帧
9224: e49db004 pop {fp} ; (ldr fp, [sp], #4)
9228: e12fff1e bx lr
0000922c <main>:
922c: e92d4800 push {fp, lr}
9230: e28db004 add fp, sp, #4 ;栈帧上边界fp
9234: e24dd008 sub sp, sp, #8 ;栈帧下边界sp
9238: e59f30d4 ldr r3, [pc, #212] ; 9314 <main+0xe8>
;取出立即数 0x12345678 放到r3中
923c: e50b300c str r3, [fp, #-12]
;把r3内容也就是0x12345678 存放到nVar(fp-12)地址处
9240: e24b300c sub r3, fp, #12 ;r3此时存放变量 nVar 的地址
9244: e50b3008 str r3, [fp, #-8] ; 把nVar 地址存放到 nVarType
;这里需要注意了,fp-8属于nVarType变量的地址,在这个地址处存放着
;变量 nVar 的地址,所以说引用也是占用内存的,只是这块内存存放的不是
;被引用对象的数值,而是被引用对象的地址
9248: e24b300c sub r3, fp, #12
924c: e1a00003 mov r0, r3
9250: ebffffe9 bl 91fc <_Z3AddRi> ;传递nVar 地址给add函数
9254: e59f00bc ldr r0, [pc, #188] ; 9318 <main+0xec>
9258: e51b1008 ldr r1, [fp, #-8] ;这里注意了,这条指令相当于把fp-8
;处的内容传递给r1,也就是 nVar 的地址给r1,这样导致
;cout << &nVarType 输出结果是 nVar 的地址。
925c: eb000dbd bl c958 <_ZNSolsEPKv>
9260: e1a03000 mov r3, r0
9264: e1a00003 mov r0, r3
9268: e59f10ac ldr r1, [pc, #172] ; 931c <main+0xf0>
926c: eb0004fe bl a66c <_ZNSolsEPFRSoS_E>
9270: e24b300c sub r3, fp, #12
9274: e59f009c ldr r0, [pc, #156] ; 9318 <main+0xec>
9278: e1a01003 mov r1, r3 ;传递nVar地址给 r1 寄存器
927c: eb000db5 bl c958 <_ZNSolsEPKv>
通过上面分析发现,引用和指针存储方式是一样的,都是使用内存空间存放地址值。定义一个引用,在本质上编译器生成的指令和定义一个指针是一样的,在反汇编下没有引用这种类型。指针虽然灵活,但是使用失误后果严重,引用就可以避免这种问题。
另外上面还分析了为什么 cout << &nVarType
输出的是 nVar 的地址,其实从编译器理解的角度还可以分析出来,这篇文章给出了一个很好的解释。
#常量
前面介绍的数据类型都是以变量形式进行演示的,在程序运行中可以修改其保存的数值。从字面理解常量是一个恒定不变的量。常量数据在程序运行前已经存在于可执行文件一个特殊的节中,加载进内存之后,常量就到了一个只读段中,此时对常量试图修改会导致错误。
比如下面一段代码:
#include <iostream>
using namespace std;
int main()
{
char *str = "hello world\n";
cout << str << endl;
str[1] ++;
}
运行结果就是段错误:
##常量的定义
在C++中可以使用宏机制#define来定义常量,也可以使用const定义一个编译器角度不可写的常量。#define定义的常量名称,编译器编译时会将代码中的宏名称替换成对应信息。宏的使用增加代码可读性,const是为了增加程序健壮性而存在的。宏与const使用如下代码清单所示:
#include <iostream>
using namespace std;
int main()
{
///定义 NUMBER_ONE 为常量1
#define NUMBER_ONE 1
///将常量 NUMBER_ONE 赋值给常量nVar
const int nVar = NUMBER_ONE;
///显示两者结果
cout << nVar << NUMBER_ONE << endl;
}
使用编译命令 $ arm-linux-c++ -static -E 2.c > out.txt
生成预处理文件
int main()
{
const int nVar = 1;
cout << nVar << 1 << endl;
}
##const和define的区别
define是一个真常量,而const却是由编译器判断实现的常量,是一个假常量。在实际中,使用const定义的变量,最终还是一个变量,只是在编译器内进行了检查,发现有修改则报错。
由于编译器在编译期间对const变量进行检查,因此被const修饰过的变量是可以修改的。利用指针获取到const修饰过的变量地址,强制将指针的const修饰去掉,就可以修改对应的数据内容。
#include <iostream>
using namespace std;
int main()
{
// 将变量 nConst 修饰为const
const int nConst = 5;
// 定义int 类型的指针,保存nConst 地址
int *pConst = (int*)&nConst;
// 修改指针pConst 并指向地址中的数据
*pConst = 6;
// 将修饰为const 的变量nConst 赋值给nVar
int nVar = nConst;
cout << nVar << endl;
}
使用 $ arm-linux-objdump -D -m arm
看一下:
000091fc <main>:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd010 sub sp, sp, #16
9208: e3a03005 mov r3, #5
920c: e50b3010 str r3, [fp, #-16] ;nConst = 5;
9210: e24b3010 sub r3, fp, #16
9214: e50b3008 str r3, [fp, #-8] ;pConst = &nConst
9218: e51b3008 ldr r3, [fp, #-8] ;r3 = pConst
921c: e3a02006 mov r2, #6 ;r2 = 6
9220: e5832000 str r2, [r3] ;*pConst = r2 = 6;
9224: e3a03005 mov r3, #5
9228: e50b300c str r3, [fp, #-12] ;nVar = r3 = 5
922c: e59f0024 ldr r0, [pc, #36] ; 9258 <main+0x5c>
9230: e51b100c ldr r1, [fp, #-12] ;r1 = nVar
9234: eb000971 bl b800 <_ZNSolsEi>
9238: e1a03000 mov r3, r0
923c: e1a00003 mov r0, r3
9240: e59f1014 ldr r1, [pc, #20] ; 925c <main+0x60>
9244: eb000469 bl a3f0 <_ZNSolsEPFRSoS_E>
9248: e3a03000 mov r3, #0
924c: e1a00003 mov r0, r3
9250: e24bd004 sub sp, fp, #4
9254: e8bd8800 pop {fp, pc}
在上述代码中,由于const修饰的变量nConst被赋值一个数字常量5,编译器在编译过程中发现nConst的初值是可知的,并且被修饰为const。之后所有使用nConst的地方都以这个可预知值替换,故intnVar=nConst;对应的汇编代码没有将nConst赋值给nVar,而是用常量值5代替。
通过上面反汇编分析可见nConst的数值已经被修改了,因此 被const修饰后,变量本质上并没有改变,还是可以修改的。#define与const两者之间还是不同的。