调试64bit程序所面临的挑战-part1

调试64bit程序所面临的挑战

如果到目前为止,你还没有调试过优化后的64bit代码,为了不落后于时代,那就赶紧去尝试吧。由于64bit下fastcall调用方式和大量通用寄存器的存在,找出任意栈帧中局部变量的值将会变得非常棘手。

本文,我将详述一些我非常喜欢的调试64bit代码的技巧。但在此之前,我们先对64bit下函数的调用方式做一个了解。

X64调用约定

如果你熟悉32位平台下的fastcall调用约定,那么对64位平台的fastcall你也会觉得很熟悉。在32位平台下,你需要掌握多种函数调用约定,而在64位平台下当前仅有fastcall一种调用约定(当然,我排除了通过__declspec(naked)实现的自定义调用方式)。

我将不会深入解释所有x64调用约定的方方面面,但是通常,x64fastcall调用约定下,函数的前4个参数通过寄存器rcx、rdx、r8和r9传递,超过4个的参数将通过栈进行传递(x86 fastcall调用约定下,函数的前两个参数通过寄存器ecx和edx传递,其余通过栈传递)

为了帮助演示x64调用约定如何工作,我编写了如下示例代码。尽管示例代码和工程应用中的场景有差别,但也能演示一些真实场景下的问题。代码如下:

#include <stdlib.h>

#include <stdio.h>

#include <windows.h>

__declspec(noinline)

void

FunctionWith4Params( int param1, intparam2, int param3,

int param4 )

{

size_t lotsOfLocalVariables1 = rand();

size_t lotsOfLocalVariables2 = rand();

size_t lotsOfLocalVariables3 = rand();

size_t lotsOfLocalVariables4 = rand();

size_t lotsOfLocalVariables5 = rand();

size_t lotsOfLocalVariables6 = rand();

DebugBreak();

printf( "Entering FunctionWith4Params( %X, %X, %X, %X )\n",

param1, param2, param3, param4 );

printf( "Local variables: %X, %X, %X, %X, %X, %X \n",

lotsOfLocalVariables1, lotsOfLocalVariables2,

lotsOfLocalVariables3, lotsOfLocalVariables4,

lotsOfLocalVariables5, lotsOfLocalVariables6 );

}

__declspec(noinline)

void

FunctionWith5Params( int param1, intparam2, int param3,

int param4, int param5 )

{

FunctionWith4Params( param5, param4, param3, param2 );

FunctionWith4Params( rand(), rand(), rand(), rand() );

}

__declspec(noinline)

void

FunctionWith6Params( int param1, intparam2, int param3,

int param4, int param5,int param6 )

{

size_t someLocalVariable1 = rand();

size_t someLocalVariable2 = rand();

printf( "Entering %s( %X, %X, %X, %X, %X, %X )\n",

"FunctionWith6Params",

param1, param2, param3, param4, param5, param6 );

FunctionWith5Params( rand(), rand(), rand(),

param1, rand() );

printf( "someLocalVariable1 = %X, someLocalVariable2 = %X\n",

someLocalVariable1, someLocalVariable2 );

}

int

main( int /*argc*/, TCHAR** /*argv*/ )

{

// I use the rand() function throughout this code to keep

// the compiler from optimizing too much.  If I had used

// constant values, the compiler would have optimized all

// of these away.

int params[] = { rand(), rand(), rand(),

rand(), rand(), rand() };

FunctionWith6Params( params[0], params[1], params[2],

params[3], params[4],params[5] );

return 0;

}

将上述代码拷贝到cpp文件中,按如下方式构建:

cl /EHa /Zi /Od /favor:INTEL64 example.cpp/link /debug

注意上面的/Od选项用于禁用所有优化。后面我将打开优化(魔法之门)。

生成exe后,按如下方式调试:

Windbg –Q –c “bu example!main;g;”example.exe

上面的命令将在windbg中运行程序,并在main函数处设置断点,然后一直运行到断点。

现在,让我们看一下当函数FunctionWith6Params被调用时栈的内容。

<<<<<<<<见后面链接中原文的图>>>>>>>>>>>>>

注意,尽管函数的前四个参数不需要通过栈传递,但是调用者,此处是main函数,为所有6个参数都分配了栈上空间。为通过寄存器传递的参数分配的栈空间通常叫做对应寄存器参数的“home space”。在上图中,xxxxxxxx所表示的位置内容是随机的,main函数并没有初始化这些地址。被调用函数可以自行决定是否将前4个寄存器参数写入这些位置。事实上,这正是未优化版本的行为,这样调试起来将非常方便,因为你可以从栈上看到函数参数。此外,windbg的栈相关参数如kb、kv将能正确的显示函数的前面几个参数。

下面就是函数FunctionWith6Params的前导代码执行后的栈示意图:

<<<<<<<<见后面链接中原文的图>>>>>>>>>>>>>

FunctionWith6Params的前导代码如下:

41 00000001`40015900 mov     dword ptr [rsp+20h],r9d

4100000001`40015905 mov     dword ptr[rsp+18h],r8d

4100000001`4001590a mov     dword ptr[rsp+10h],edx

4100000001`4001590e mov     dword ptr[rsp+8],ecx

4100000001`40015912 push    rbx

4100000001`40015913 push    rsi

4100000001`40015914 push    rdi

4100000001`40015915 sub     rsp,50h

可以看到,前4条指令用于将通过寄存器传过来的参数写入由main函数分配的对应的”home space”当中了,然后前导代码将所有自身打算使用的非易失性寄存器保存到栈上(在函数返回之前会从栈上恢复这些寄存器)。最后,前导代码通过操作rsp寄存器预留栈空间,这里是0x50字节。

那么这些预留的栈空间有何用了?首先,用于保存所有局部变量,FunctionWith6Params中有2个局部变量。但是这2个局部变量总大小为0x10字节,那剩下的栈空间用来干嘛了?在X64平台下,当为调用函数准备栈空间的时候,不会像X86平台那样使用push指令来将参数压入栈中。而是,对于一个函数执行的时候,它的栈指针一般不会改变。编译器在编译函数的时候会搜索该函数调用的参数最多的函数具有几个参数,据此来分配函数的栈空间。在这个例子中,FunctionWith6Params中调用printf使用了8个参数,因此编译器为该函数在栈上分配了8个位置。最上面的4个位置将作为所有FunctionWith6Params中调用的函数的home
space。

X64调用约定带来的一个有趣的地方就是一旦你执行完一个函数的前导代码,栈指针在执行结尾代码之前都将保持不变。这样就舍弃了X86平台下需要的基址寄存器。当在函数FunctionWith6Params中进行函数调用时,前4个参数通过寄存器传递,然后使用mov指令将剩下的参数放入已经分配好的栈空间上。

调试优化过的X64代码(噩梦的开始)

为何调试优化过的X64代码如此棘手?还记得调用函数为通过寄存器传递的函数分配的home space吗?在未优化的时候,被调用函数将会把通过寄存器传递的参数写入home space当中,但是,这一步在优化过后就没有了。

继续采用如下的参数编译之前的代码:

cl /EHa /Zi /Ox /favor:INTEL64 example.cpp/link /debug

注意这里使用的是/Ox选项开启了最大优化。仍然打开了调试符号生成来方便我们调试代码。始终开启调试信息来构建你的外发产品,这样你才能调试你的外发产品。

让我们看看FunctionWith6Params的前导代码:

4100000001`400158e0 mov     qword ptr[rsp+8],rbx

4100000001`400158e5 mov     qword ptr[rsp+10h],rbp

4100000001`400158ea mov     qword ptr[rsp+18h],rsi

4100000001`400158ef push    rdi

4100000001`400158f0 push    r12

4100000001`400158f2 push    r13

4100000001`400158f4 sub     rsp,40h

4100000001`400158f8 mov     ebx,r9d

4100000001`400158fb mov     edi,r8d

4100000001`400158fe mov     esi,edx

4100000001`40015900 mov     r12d,ecx

优化后的代码跟之前的完全不一样了。总结下来变化有:

l  函数使用了栈上的homespace。但是它不是用来存储前4个参数,而是用来保存非易失性寄存器的值,由于优化后的代码将使用更多的寄存器,导致更多的非易失性寄存器被保存。

l  三个寄存器值被保存在栈上,还有3个被保存在home space处。

l  然后分配了栈空间。但是相比非优化代码,仅用了栈上0x40字节。这是因为优化后的代码使用寄存器来表示局部变量someLocalVariable1和someLocalVariable2。因此函数仅需要为调用函数printf函数分配8个空间就可以了。

l  然后代码将前4个参数保存到了非易失寄存器当中,而不是栈上的home space。(不要依赖这个机制,一个被优化过的函数可能不会对rcx、rdx、r8、r9做拷贝,具体依赖于代码结构)

现在单步调试到第一个printf调用之后。我机器上printf的输出是:

Entering FunctionWith6Params( 29, 4823,18BE, 6784, 4AE1, 3D6C )

Windbg的常用栈显示命令是kb,这将显示出函数的前面几个参数。事实上,它显示的是栈的前面小部分的内容。Kb命令的输出是:

0:000> kb

RetAddr           : Args to Child                                                          : Call Site

00000001`4001593b : 00000000`00004ae100000000`00004823 00000000`000018be 00000000`007e3570 :example!FunctionWith6Params+0x6a [c:\temp\blog_entry\sample_code\example.cpp @37]

00000001`40001667 : 00000000`0000000000000000`00000000 00000000`00000000 00000000`00000001 : example!main+0x5b[c:\temp\blog_entry\sample_code\example.cpp @ 57]

00000000`76d7495d : 00000000`0000000000000000`00000000 00000000`00000000 00000000`00000000 :example!__tmainCRTStartup+0x15b

00000000`76f78791 : 00000000`0000000000000000`00000000 00000000`00000000 00000000`00000000 :kernel32!BaseThreadInitThunk+0xd

00000000`00000000 : 00000000`0000000000000000`00000000 00000000`00000000 00000000`00000000 :ntdll!RtlUserThreadStart+0x1d

注意,FunctionWith6Params的前四个参数和kb显示的参数不完全相同,这就是优化带来的副作用。对于优化过的代码,你不能在相信kv、kb给出的结果了。这就是为什么64bit代码如此难以调试的原因。相信我,上面第3个和第4个参数和实际相符仅仅是因为巧合。

参数推演—技巧1(向下推演)

现在,让我们看一些从64位函数调用栈中找出函数参数的方法。为了演示用,我在函数FunctionWith4Params中加入了一个DebugBreak调用。在windbg中执行程序直到命中断点。现在,假定你所看到的是一个来自用户的dump的场景,你的程序就crash在这个点。你用KL命令看到的栈如下:

0:000> kL

Child-SP          RetAddr           Call Site

00000000`0012fdc8 00000001`40015816ntdll!DbgBreakPoint

00000000`0012fdd0 00000001`400158a0example!FunctionWith4Params+0x66

00000000`0012fe50 00000001`40015977example!FunctionWith5Params+0x20

00000000`0012fe80 00000001`40015a0bexample!FunctionWith6Params+0x97

00000000`0012fee0 00000001`4000168bexample!main+0x5b

00000000`0012ff20 00000000`7733495dexample!__tmainCRTStartup+0x15b

00000000`0012ff60 00000000`77538791kernel32!BaseThreadInitThunk+0xd

00000000`0012ff90 00000000`00000000ntdll!RtlUserThreadStart+0x1d

现在,我们认为你必须找到FunctionWith6Params的调用参数才能找出问题。(假设你没有看到控制台输出的参数,不许作弊)

我所要演示给你的第一个技巧是观察进入函数FunctionWith6Params后,代码对rcx做了哪些操作。这里,由于参数是32位的,我们将跟踪ecx的值。

让我们从FunctionWith6Params中开始准备调用函数FunctionWith5Params的代码看起:

0:000> u example!FunctionWith6Paramsexample!FunctionWith6Params+0x97

example!FunctionWith6Params[c:\temp\blog_entry\sample_code\example.cpp @ 41]:

00000001`400158e0 mov     qword ptr [rsp+8],rbx

00000001`400158e5 mov     qword ptr [rsp+10h],rbp

00000001`400158ea mov     qword ptr [rsp+18h],rsi

00000001`400158ef push    rdi

00000001`400158f0 push    r12

00000001`400158f2 push    r13

00000001`400158f4 sub     rsp,40h

00000001`400158f8 mov     ebx,r9d

00000001`400158fb mov     edi,r8d

00000001`400158fe mov     esi,edx

00000001`40015900 mov     r12d,ecx

00000001`40015903 call    example!rand (00000001`4000148c)

00000001`40015908 movsxd  r13,eax

00000001`4001590b call    example!rand (00000001`4000148c)

00000001`40015910 lea     rdx,[example!`string‘+0x68(00000001`40020d40)]

00000001`40015917 movsxd  rbp,eax

00000001`4001591a mov     eax,dword ptr [rsp+88h]

00000001`40015921 lea     rcx,[example!`string‘+0x80(00000001`40020d58)]

00000001`40015928 mov     dword ptr [rsp+38h],eax

00000001`4001592c mov     eax,dword ptr [rsp+80h]

00000001`40015933 mov     r9d,esi

00000001`40015936 mov     dword ptr [rsp+30h],eax

00000001`4001593a mov     r8d,r12d

00000001`4001593d mov     dword ptr [rsp+28h],ebx

00000001`40015941 mov     dword ptr [rsp+20h],edi

00000001`40015945 call    example!printf (00000001`400012bc)

00000001`4001594a call    example!rand (00000001`4000148c)

00000001`4001594f mov     edi,eax

00000001`40015951 call    example!rand (00000001`4000148c)

00000001`40015956 mov     esi,eax

00000001`40015958 call    example!rand (00000001`4000148c)

00000001`4001595d mov     ebx,eax

00000001`4001595f call    example!rand (00000001`4000148c)

00000001`40015964 mov     r9d,r12d

00000001`40015967 mov     r8d,esi

00000001`4001596a mov     edx,ebx

00000001`4001596c mov     ecx,eax

00000001`4001596e mov     dword ptr [rsp+20h],edi

00000001`40015972 call    example!ILT+5(?FunctionWith5ParamsYAXHHHHHZ)(00000001`4000100a)

在函数FunctionWith6Params中,会将ecx拷贝到r12d

参数推演—技巧2(向上推演)

参数推演—技巧3(死区探查)

参数推演—技巧4(非易失性寄存器)

非易失性寄存器和易失性寄存器列表:

https://msdn.microsoft.com/en-us/library/9z1stfyw.aspx

原文:

http://blogs.msdn.com/b/ntdebugging/archive/2009/01/09/challenges-of-debugging-optimized-x64-code.aspx

时间: 2024-10-27 03:36:50

调试64bit程序所面临的挑战-part1的相关文章

OC中并发编程的相关API和面临的挑战

OC中并发编程的相关API和面临的挑战(1) 小引 http://www.objc.io/站点主要以杂志的形式,深入挖掘在OC中的最佳编程实践和高级技术,每个月探讨一个主题,每个主题都会有几篇相关的文章出炉,2013年7月份的主题是并发编程,今天挑选其中的第2篇文章(Concurrent Programming: APIs and Challenges)进行翻译,与大家分享一下主要内容.由于内容比较多,我将分两部分翻译(API和难点)完成,翻译中,如有错误,还请指正. 目录 1.介绍 2.OS

MAC使用IDA PRO远程调试LINUX程序

1 背景 在学习Linux系统上的一些漏洞知识的时候,往往需要进行“实地测试”,但是在Linux系统上进行调试并不太方便,因为LINUX自带的GDB调试工具真的不太人性化,即使有GDBTUI之类的“伪图形界面调试器”,也跟IDA PRO之类的调试器相差甚远.这里又遇到另一个问题了——LINUX平台的IDA PRO不太好找. 综上,对于初学者而言最佳方案就是使用IDA PRO的远程调试功能! 2 环境配置 本文主要说明在MAC系统上如何通过IDA PRO对linux程序进行远程调试(如果宿主机为W

使用 Eclipse 调试 Java 程序的技巧【9】

若有不正之处,请多多谅解并欢迎批评指正,不甚感激.请尊重作者劳动成果: 本文原创作者:pipi-changing本文原创出处:http://www.cnblogs.com/pipi-changing/ 本文版权归作者和博客园共有,未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接 ,否则保留追究法律责任的权利. 使用 Eclipse 调试 Java 程序的技巧 不要调试太多 只调试你觉得有问题的代码或者一部分一部分功能的调试: [ 断点视图 : 条件断点] 如果你只对应用中的某部分感

使用Eclipse调试Java 程序的10个技巧

你应该看过一些如<关于调试的N件事>这类很流行的帖子 .假设我每天花费1小时在调试我的应用程序上的话,那累积起来的话也是很大量的时间.由于这个原因,用这些时间来重视并了解所有使我们调试更方便的功能. 那能为你省下一些时间,也将会使你的生活更安逸.轻松.同时也表明其它关于此主题的帖子也是很有价值的. 第1条:不要调试太多 一个关于调试的疯狂声明作为开头.但它必须是要说的!尝试切分一下你那复杂的逻辑成多个独立的单元,并编写单元测试来检测你代码的正确性.我想像如下 这样的流程应该是发生得非常频繁的-

移动APP支付面临巨大挑战,安全成发展“命门”

依据<中国第三方网络支付安全调研报告>,现如今移动互联网的飞速发展,网络支付为人们带来极大便利,APP的安全性也一直备受关注.山寨APP一方面,用户的身份.银行财产等相关数据和手机应用的绑定越来越紧密,另一方面,伺机而动的黑客们也抓住了这送上门的机会,在移动支付领域兴风作浪,将黑手伸进手机钱包甚至银行账户里面,移动支付安全面临巨大挑战. 据业内人士表示,当前阶段移动支付安全问题,已经成为整个行业的痛点,甚至是制约整个行业良性发展的绊脚石.据有关数据显示,目前在网民面临的各类安全问题中,&quo

医疗保健系统的网络安全 面临严峻挑战

作为致力于建立良好网络安全的厂商,趋势科技一直以来都扮演着安全产业内的两个角色.一个是日以继夜.不眠不休地努力让世界各地企业.政府和消费者免于最新威胁所害:另一个则是致力于让个人和组织都能更加认识威胁情势的变化,以打造一个更为安全的数字信息交流的世界. 而医疗保健是我们这两个身份目前都在努力中的目标.一方面,有越来越多网络犯罪分子将其当作目标,另一方面,也有部分组织对于病患健康数据(PHI)的外泄威胁反应迟缓. 医疗保健方面的资料入侵外泄占美国资料入侵外泄事件的43%  医疗保健记录对网络犯罪分

Web开发面临的挑战主要有哪些?

摘要:要成为一名高效的Web开发者,这需要我们做很多工作,来提高我们的工作方式,以及改善我们的劳动成果.而在开发中难免会遇到一些困难,从前端到后端. 导读:要成为一名高效的Web开发者,这需要我们做很多工作,来提高我们的工作方式,以及改善我们的劳动成果.而在开发中难免会遇到一些困难,从前端到后端,近日,在问答网站知乎上,有人抛出了“Web前端开发面临的挑战主要有哪些?”和“后端开发主要的挑战有哪些?”话题,众技术大牛各抒己见,CSDN软件研发频道对本文内容进行了整理,方便大家学习与参考. 一.W

亲测VS2010纯静态编译QT4.8.0,实现VS2010编译调试Qt程序,QtCreator静态发布程序(图文并茂,非常详细)

下载源代码,注意一定是源码压缩包如qt-everywhere-opensource-src-4.8.0.zip,不是Qt发布的已编译的不同版本的标准库如qt-win-opensource-4.8.0-vs2010.exe,这些版本都只是动态编译的,不是我们所需要的.只有用源码包才能做真正的纯静态编译,如果用Qt官网已编译的标准库.exe文件进行编译,有可能出现各种错误,而且编译后的文件巨大,我当时就在这里绕了很大的弯子,请童鞋们注意啦!!可能有些老鸟会笑话,但我本着最严肃的态度告诉菜鸟们,上网查

使用VS+VisualGDB编译调试Linux程序

Linux程序开发变得越来越多,越来越多的程序.产品需要跨平台,甚至有些开源项目只支持Linux平台,所以掌握Linux开发变得越来越重要. 但是对于习惯了Windows下的开发,使用了VS这个宇宙第一IDE后,觉得Linux下的纯命令行开发调试,还是有些不习惯,效率有些低(大神除外).那么能不能用VS来开发调试Linux程序呢,经过各种查找,找到了VisualGDB这个神奇的插件,通过VS+VisualGDB就可以编译调试Linux程序. 下面我们来看一下创建demo的过程吧. 配置虚拟机 V