《天书夜读:从汇编语言到windows内核编程》三 练习反汇编C语言程序
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 ...
随机推荐
- 规划自己的生活,从使用GTD时间管理法开始
前言 为了不再浪费时间,不在茫然度过每一天,我为自己应用了GTD时间管理法,之前并不知道这种方法,实际和我自己定制的也差不太多,下面说说这个方法. 一.GTD时间管理 时间管理法有很多,而GTD( ...
- Another app is currently holding the yum lock; waiting for it to exit 解决方法
Another app is currently holding the yum lock; waiting for it to exit... The other application is: P ...
- Opencv处理鼠标事件-OpenCV步步精深
在图片上双击过的位置绘制一个 圆圈 鼠标事件就是和鼠标有关的,比如左键按下,左键松开,右键按下,右键松开,双击右键等等. 我们可以通过鼠标事件获得与鼠标对应的图片上的坐标.我们通过以下函数来调用查看所 ...
- git subtree pull 错误 Working tree has modifications
git subtree 是不错的东西,用于 git 管理子项目. 本文记录我遇到问题和翻译网上的答案. 当我开始 pull 的时候,使用下面的代码 git subtree pull --prefix= ...
- win10 uwp 存放网络图片到本地
有时候我们的网络很垃圾,我的的UWP要在第一次打开网络图片,就把图片存放到本地,下次可以从本地打开. 有时候用户使用的是流量网络,不能每次都联网下载. 我们不得在应用存放用户打开的图片. 这就是先把图 ...
- MySQL基础函数
MySQL数据库提供了很多函数包括: 数学函数: 字符串函数: 日期和时间函数: 条件判断函数: 系统信息函数: 加密函数: 格式化函数: 一.数学函数 数学函数主要用于处理数字,包括整型.浮点数等. ...
- 案例:AWR手工创建快照失败,SYSAUX表空间剩余不足处理
案例:AWR手工创建快照失败,SYSAUX表空间剩余不足处理 版本:Oracle 11.2.0.4 RAC 问题现象:AWR手工创建快照失败,SYSAUX表空间剩余不足. 1. 查看SYSAUX表空间 ...
- ASP动态网站建设之连接数据库相关操作
连接数据库: string str = @"server=服务器名称;Integrated Security=SSPI;database=数据库名称;"; 注意封装公共类,将常用重 ...
- Fastify 系列教程二 (中间件、钩子函数和装饰器)
Fastify 系列教程: Fastify 系列教程一 (路由和日志) Fastify 系列教程二 (中间件.钩子函数和装饰器) 中间件 Fastify 提供了与 Express 和 Restify ...
- sql语句中生成0-10随机数
DECLARE @i int=0;DECLARE @j decimal(9,2);DECLARE @qnum INT=1000; SET NOCOUNT ONCREATE TABLE #temp_Ta ...