1) Debug版本算法反汇编,现有如下3×3矩阵相乘的程序:
#define SIZE 3 int MyFunction(int a[SIZE][SIZE],int b[SIZE][SIZE],int c[SIZE][SIZE]) { int i,j; ; i < ; i++ ) { ; j < ; j++ ) { c[i][j] = a[i][]*b[][j] + a[i][]*b[][j] + a[i][]*b[][j]; } } ; }
Debug版本汇编后为:
#define SIZE int MyFunction(int a[SIZE][SIZE],int b[SIZE][SIZE],int c[SIZE][SIZE]) { push ebp mov ebp,esp sub esp,48h ;48H字节局部变量存储区 push ebx push esi push edi lea edi,[ebp-48h] 0040102C mov ecx,12h mov eax,0CCCCCCCCh rep stos dword ptr [edi] int i,j; for ( i = ; i < 3 ; i++ ) ], ;ebp – 4 为局部变量i 0040103F jmp MyFunction+2Ah (0040104a) ] ],eax ], 0040104E jge MyFunction+0AAh (004010ca) ;标准for循环语句 { for ( j = ; j < 3 ; j++ ) ], ;ebp – 8 为局部变量j ) ] ],ecx ], jge MyFunction+0A5h (004010c5) ;标准for循环语句 { c[i][j] = a[i][]*b[][j] + a[i][]*b[][j] + a[i][]*b[][j]; ] ;取i值 0040106B imul edx,edx,0Ch ;一级偏移:第一下标[i] ] ;ebp+8为第1参数:数组a ] ;取j值 mov esi,dword ptr [ebp+0Ch] ;ebp+C为第2参数:数组b mov edx,dword ptr [eax+edx] ;a[i][0] ] ;a[i][0] * b[0][j] -> edx ] imul eax,eax,0Ch ] ] 0040108A mov edi,dword ptr [ebp+0Ch] ] ;a[i][1] +0Ch]; a[i][1] * b[1][j] ->eax ;这里注意:0CH = b[1]–b[0] = ( 1- 0 ) * SIZE * sizeof( int ) add edx,eax ;edx + eax -> edx ;即edx = a[i][0] * b[0][j] + a[i][1] * b[1][j] ] 0040109B imul ecx,ecx,0Ch ] ] 004010A4 mov edi,dword ptr [ebp+0Ch] ] ; a[i][2] +18h]; a[i][2] * b[2][j] ->ecx ;同上:018H = b[2]–b[0] = ( 2- 0 ) * SIZE * sizeof( int ) 004010B0 add edx,ecx ;edx + eax -> edx ;即edx = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j] ] 004010B5 imul eax,eax,0Ch 004010B8 mov ecx,dword ptr [ebp+10h] ;ebp+10第3参数:数组c 004010BB add ecx,eax ;c[i] -> ecx ] ],edx ;edx -> c[i][j] } ) ;内层循环 } ) ;外层循环 return ; 004010CA xor eax,eax ;返回0 } 004010CC … //省略现场恢复代码
再看看这个函数的调用,使用如下代码:
int main(void) { ,,,,,,,,}; ,,,,,,,,}; int c[SIZE][SIZE]; MyFunction(a,b,c); ; }
对应的汇编代码为:
int main(void) { push ebp ;将看到标准现场保护 mov ebp,esp ;ebp为栈基址指针 sub esp,0Ach ;临时取堆栈大小0ACH push ebx 0040110A push esi 0040110B push edi 0040110C lea edi,[ebp-0ACh] ;起点 mov ecx,2Bh ;循环次数 mov eax,0CCCCCCCCh ;所赋的值 0040111C rep stos dword ptr [edi] ;初始化临时区 ,,,,,,,,}; ;以下为局部变量初始化 ;a数组起始地址epb – 24H ;ebp为栈基址指针 ;故这里的地址是逐渐增大 ], ], ;赋值 ,,,,,,,,}; ;b数组起始地址ebp – 48H int c[SIZE][SIZE]; MyFunction(a,b,c); 0040119C lea eax,[ebp-6Ch] ;c数组起始地址ebp – 6CH 0040119F push eax ;传递第3个参数(c) 004011A0 lea ecx,[ebp-48h] 004011A3 push ecx ;传递第2个参数(b) 004011A4 lea edx,[ebp-24h] 004011A7 push edx ;传递第1个参数(a) (MyFunction) () 004011AD add esp,0Ch ;恢复堆栈 return ; 004011B0 xor eax,eax } 004011B2 … //省略现场恢复
第一:C语言在函数入口时在栈区开辟临时存储区来存储局部变量,这些局部变量的生存周期一直到函数返回;
第二:局部变量的声明并不会对其进行赋值,只有在赋值语句出现时,才对内存进行读写;
第三:C语言中不管以什么方式传递数组,传递的永远是指针值(它在MyFunction函数中并没有对传递过来的数组实行一次拷贝);
第四:所有的下标运算符同点操作符一样被转为偏移量的形式;
第五:读Debug版汇编很简单,写Debug版汇编是一个挑战
2) 阅读汇编代码技巧:先分清楚控制流程代码与数值计算代码。对数值计算的代码判断输入与输出(被读的内部变量为输入,被写的内部变量为输出),还原成C语言。任何一段中间不带跳转的连续MOV和加减乘除指令均可还原为一个C表达式。
3) 对于二(2)中的for循环函数:
int MyFunction(int a,int b) { int c = a + b; int i; ; i < ; i ++ ) { c = c + i; } return c; }
在Release版本中反汇编为(我采用的是OllyICE反汇编,其中RETN及为RET):
可以看到与Debug版本的汇编代码大相径庭:第一:没有现场保护与现场恢复代码;第二:并没有为参数以及自定义的局部变量分配临时存储区域;第三:for循环的结构显然被改写。
先看看这个函数是怎么被调用的:
可以看到,调用代码与Debug版本并没有区别,先把参数1,2反序压入堆栈,随后执行调用函数,最后恢复堆栈指针(这里的返回0不用理会,Release汇编以后,在函数入口(主函数)中的函数调用会另外生成一个函数来执行,即上面所看到的代码段)。
综上:
第一:Release版本会对代码执行优化,甚至不保存EBP,完全不做现场保护与恢复工作;
第二:对于数量较少的局部变量,Release版本并不会为它们在堆栈上分配临时变量存储区域,这意味着ESP也不需要进行操作(RET指令除外),取参数直接用ESP加上偏移来取(第一个参数偏移4,是因为执行CALL调用的时候实现了一次压栈);
第三:既然不分配临时变量存储区域,那么临时变量将使用寄存器来存放,这与C语言中指定的寄存器变量等同?目前我还不清楚。不过对于频繁操作的变量(如本例的计数器局部变量i)会自动优化为使用寄存器;
第四:内部的结构可能会被部分篡改或者完全改变,无法和原C语言一一对应(如本例优化后的for循环使用了相对简单的do循环方式,把判断和跳转放到了最后)
下面看看三(1)中矩阵相乘的例子,先简单看下调用代码是否有区别:
简要分析如下:
第1行:分配局部变量栈区06CH( = 108 )字节(32位下相当于27个int型数值)
第2~4行:将常数7、8、9移动到寄存器
第5~11行(排除第9行):对数组a、b赋值7,8,9
第12~16行(外加第9行):取数组c、b、a地址并压入堆栈实现参数传递
第17~22行:对数组b其余单元进行赋值
第23~28行:对数组a其余单元进行赋值
第29行:执行函数调用
第30行:清EAX,即设置返回值为0
第31行:恢复堆栈,078H = 06CH(局部变量堆栈区) + 08H(3次PUSH操作)
对与赋值的与参数取址传递的过程很不和谐,直到函数调用前,堆栈区情况如下:
堆栈地址 |
值(HEX) |
描述 |
改变代码行号 |
… |
… |
非局部变量堆栈区 |
|
0012FF0C |
0012FF3C |
第1个参数,a数组起始地址 |
16 |
0012FF10 |
0012FF18 |
第2个参数,b数组起始地址 |
15 |
0012FF14 |
0012FF60 |
第3个参数,c数组起始地址 |
13 |
0012FF18 |
00000009 |
b[0][0]:b数组起始地址 |
6 |
0012FF1C |
00000008 |
b[0][1] |
8 |
0012FF20 |
00000007 |
b[0][2] |
11 |
0012FF24 |
00000006 |
b[1][0] |
23 |
0012FF28 |
00000005 |
b[1][1] |
24 |
0012FF2C |
00000004 |
b[1][2] |
25 |
0012FF30 |
00000003 |
b[2][0] |
26 |
0012FF34 |
00000002 |
b[2][1] |
27 |
0012FF38 |
00000001 |
b[2][2] |
28 |
0012FF3C |
00000001 |
a[0][0]:a数组起始地址 |
17 |
0012FF40 |
00000002 |
a[0][1] |
18 |
0012FF44 |
00000003 |
a[0][2] |
19 |
0012FF48 |
00000004 |
a[1][0] |
20 |
0012FF4C |
00000005 |
a[1][1] |
21 |
0012FF50 |
00000006 |
a[1][2] |
22 |
0012FF54 |
00000007 |
a[2][0] |
10 |
0012FF58 |
00000008 |
a[2][1] |
7 |
0012FF5C |
00000009 |
a[2][2] |
5 |
0012FF60 |
随机值 |
c[0][0]:c数组起始地址 |
|
0012FF64 |
随机值 |
c[0][1] |
|
0012FF68 |
随机值 |
c[0][2] |
|
0012FF6C |
随机值 |
c[1][0] |
|
0012FF70 |
随机值 |
c[1][1] |
|
0012FF74 |
随机值 |
c[1][2] |
|
0012FF78 |
随机值 |
c[2][0] |
|
0012FF7C |
随机值 |
c[2][1] |
|
0012FF80 |
随机值 |
c[2][2] |
|
0012FF84 |
… |
非局部变量堆栈区 |
|
综上:
第一:Release总是最大限度的使用寄存器(如本例的赋值拆成了两部分,把7、8、9放入寄存器后赋值比使用立即数赋值更高效?暂时不得而知);
第二:Release汇编总是使用尽量少的临时堆栈区(如这里使用3个大小为9的二维int型数组,刚好108个字节,编译器并没有进行多余的分配,这与Debug版本不同,Debug下一般都会保证一定余量);
第三:Release汇编代码与C语句并没有一一对应的关系,顺序结构完全可能被打乱,必须从操作的结果来分析操作的目的(如这里是对数组的初始化与函数调用,但初始化被参数压栈“打断”)
可见,Release版本虽然汇编代码与Debug版本很多差别,但是完成的功能类似,下面看看MyFunction的反汇编代码
详细分析如下(这里数据段DS与堆栈段基址SS相等):
第1行:取第1个参数(数组a起始地址)到EAX
第2行:取第3个参数(数组c起始地址)到EDX
第3~6行:现场保护
第7行:ECX = a[0][1]
第8行:寄存器EDI作为外层循环计数器,赋初值EDI = 3
第9行:函数被调用以后,ESP被移动了5次(一次CALL+4次PUSH),则这里的ESP+18H实际上指向的是调用前的ESP + 18H – 20 = ESP + 4,即指向第2个函数参数。故此处为取第2个参数(数组b起始地址)到EAX
第10行:ESI作为内层循环计数器,赋初值3
第11~13行:EAX = b[1][0],EBX = b[2][0],EBP = b[0][0],即取b数组的一列(第一次循环是第1列,下内层循环一次,移动到下一列)
第14~15行:EBX = a[0][2] * b[2][0],EBP = a[0][0] * b[0][0],a的第1行,与上类似
第16行:EBX = a[0][2] * b[2][0] + a[0][0] * b[0][0]
第17~19行:EBX = a[0][2] * b[2][0] + a[0][1] * b[1][0] + a[0][0] * b[0][0]
第20行:EXA = EXA + 4,即指向下一列
第21~22行:将结果EBX写入数组c,c向后移动一个单位
第23~24行:内层循环计数器减1,循环判断
第25行:a数组移动到下一行
第26~27行:外层循环计数器减1,循环判断
第28~32行:置返回值为0,恢复现场
综上:
第一:两个for循环均采用了do-while循环的形式;
第二:打乱了c[i][j] = a[i][0]*b[0][j] + a[i][1]*b[1][j] + a[i][2]*b[2][j]从左到右的相加次序;
第三:与Debug下汇编相比,大量使用寄存器来存储变量,数组下标的偏移变的隐晦;
第四:并非所有Release版反汇编都可以还原为编译之前的C语言代码,但是还原成功能等价的C语言代码还是可能的
总述:读Release版汇编很蛋疼,写Release版汇编是不可能!
说明:这里看到的汇编源代码与原书并不相同,原书仅仅作为引导,我以实际为准自行分析与学习。所有如有错误,与原书无关。
4) 阅读Release汇编代码时,如2)所言先分清楚控制流程代码与数值计算代码。对数值计算代码的阅读可以使用逆向的方法:先判断输出,如在3*3矩阵乘法Release版本汇编代码时,先标记出所有控制流程的代码,判断为双层嵌套循环,形式如:
int i,j; ; i < ; i ++ ) { //dosomething ; j < ; j ++ ) { //dosomething } }
然后进行数值计算代码分析时,先找到输出变量,这里由语句找到语句MOV PTR DWORD DS:[EDX],EBX可以判断,DS:[EDX]是输出变量,而且整段代码只有这一处对它进行了修改,然后逆向追踪EBX,从而最终得到由已知输入变量计算得到输出变量的计算表达式。
《天书夜读:从汇编语言到windows内核编程》三 练习反汇编C语言程序的更多相关文章
-
《天书夜读:从汇编语言到windows内核编程》五 WDM驱动开发环境搭建
(原书)所有内核空间共享,DriverEntery是内核程序入口,在内核程序被加载时,这个函数被调用,加载入的进程为system进程,xp下它的pid是4.内核程序的编写有一定的规则: 不能调用win ...
-
《天书夜读:从汇编语言到windows内核编程》八 文件操作与注册表操作
1)Windows运用程序的文件与注册表操作进入R0层之后,都有对应的内核函数实现.在windows内核中,无论打开的是文件.注册表或者设备,都需要使用InitializeObjectAttribut ...
-
《天书夜读:从汇编语言到windows内核编程》十一 用C++编写内核程序
---恢复内容开始--- 1) C++的"高级"特性,是它的优点也是它的缺点,微软对于使用C++写内核程序即不推崇也不排斥,使用C++写驱动需注意: a)New等操作符不能直接使用 ...
-
《天书夜读:从汇编语言到windows内核编程》六 驱动、设备、与请求
1)跳入到基础篇的内核编程第7章,驱动入口函数DriverEnter的返回值决定驱动程序是否加载成功,当打算反汇编阅读驱动内核程序时,可寻找该位置. 2)DRIVER_OBJECT下的派遣函数(分发函 ...
-
《天书夜读:从汇编语言到windows内核编程》四 windows内核调试环境搭建
1) 基础篇是讲理论的,先跳过去,看不到代码运行的效果要去记代码是一个痛苦的事情.这里先跳入探索篇.其实今天的确也很痛苦,这作者对驱动开发的编译与调试环境介绍得太模糊了,我是各种尝试,对这个环境的搭建 ...
-
《天书夜读:从汇编语言到windows内核编程》十 线程与事件
1)驱动中使用到的线程是系统线程,在system进程中.创建线程API函数:PsCreateSystemThread:结束线程(线程内自行调用)API函数:PsTerminateSystemThrea ...
-
《天书夜读:从汇编语言到windows内核编程》九 时间与定时器
1)使用如下自定义函数获取自系统启动后经历的毫秒数:KeQueryTimeIncrement.KeQueryTickCount void MyGetTickCount(PULONG msec) { L ...
-
《天书夜读:从汇编语言到windows内核编程》七 内核字符串与内存
1)驱动中的字符串使用如下结构: typedef struct _UNICODE_STRING{ USHORT Length; //字符串的长度(字节数) USHORT MaximumLength; ...
-
《天书夜读:从汇编语言到windows内核编程》二 C语言的流程与处理
1) Debug与Release的区别:前者称调试版,后者称发行版.调试版基本不优化,而发行版会经过编译器的极致优化,往往与优化前的高级语言执行流程会大相径庭,但是实现的功能是等价的. 2) 如下fo ...
随机推荐
-
Windows下的Eclipse启动出现:a java runtime environment(JRE) or java development kit(JDK) must be
打开eclipse的时候回遇到这种情况 解决方案: 进入eclipse.exe所在的目录,在eclipse.ini文件中加入以下两行: -vm <your path to jdk|jre> ...
-
Java知多少(111)数据库之修改记录
修改数据表记录也有3种方案. 一.使用Statement对象 实现修改数据表记录的SQL语句的语法是: update表名 set 字段名1 = 字段值1,字段名2 = 字段值2,……where特 ...
-
Qt学习总结-ui篇(二)
qccs定义圆角 border-radius:10px; 如果想给特定位置定义圆角,如: 左上角:border-left-top-radius:10px; 右下角色:border-right-bo ...
-
我是如何理解ThreadLocal
ThreadLocal的概念 ThreadLocal从英文的角度看,可以看成thread和local的组合,就是线程本地的意思,我们都知道,看过jvm内存分配的人都知道在jvm虚拟机中对每一个线程都分 ...
-
TabTopAutoLayout【自定义顶部选项卡区域(带下划线)(动态选项卡数据且可滑动)】
版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 自定义顶部选项卡布局LinearLayout类,实现带下划线且可滑动效果.[实际情况中建议使用RecyclerView] 备注:如果 ...
-
Android Wear 2.0 AlarmManager 后台定时任务
以前在Android 4.0时,alarmManager 没什么问题.后来android为了优化系统耗电情况,引入了doze模式,参见此页 https://developer.android.com/ ...
-
<;Dare To Dream>; 第四次作业:基于原型的团队项目需求调研与分析
任务1:实施团队项目软件用户调研活动. (1)真实的用户调研对象:生科院大三学生 (2)利用实验七所开发的软件原型:网站原型链接 (3)要有除原型法之外的其他需求获取手段: 访谈法 开会研讨法 (4) ...
-
UMEditor(Ueditor mini)修改图片上传路径
UMEditor(Ueditor mini)修改图片上传路径 imageUp.ashx string pathbase = "/UpLoad/images/"; //保存文件夹在网 ...
-
TCP拥塞控制及连接管理
在阅读此篇之前,博主强烈建议先看看TCP可靠传输及流量控制. 一.TCP拥塞控制 在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏——产生拥塞(congestion ...
-
Vue.js的小例子--随便写的
1.领导安排明天给同事们科普下vue 2.简单写了两个小例子 3.话不多说直接上代码 <!DOCTYPE html> <html> <head> <meta ...