`
webcenterol
  • 浏览: 914928 次
文章分类
社区版块
存档分类
最新评论

在C++中内嵌汇编代码分析

 
阅读更多

C++中内嵌汇编代码分析

write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie

JAVA或者Python的人常常会告诉你,现在的硬件已经太快了,以至于你可以完全不再考虑性能,快速的开发才是最最重要的。这是个速食者的年代,什么都是,甚至是编程。

但是。。。。。。永远有但是,任何C/C++程序员在工作一段时间后都会发现,不关心性能的C/C++程序员是干不下去的。。。。。C语言的程序员犹甚,C++的程序员也许还可以光靠着MFC等类库混口饭吃。。。

C/C++程序员常常会发现自己只有有限的内存,要占用更小的CPU资源(或者仅仅只有速度非常有限的嵌入式CPU可用),但是却得完成非常复杂的任务,这时候能够利用的工具却非常有限。。。唯一可能有用的就是自己的大脑。。。呵呵,改进算法永远是最重要的,但是一个非常优秀的算法也没有办法满足要求的时候,你再剩下的东西可能就是无限的优化原有的C/C++代码,接着就是汇编了。

这里从内嵌汇编将起,然后讲讲用MASM独立编译,然后链接的方法,都以VS2005为例子,其他编译器请参考。

内嵌汇编:

内嵌汇编的使用是很简单并且方便的,在VS中以的形式就可以简单的使用。对于此语法我没有必要再多说了,各类资料都有介绍,可以参考参考《加密与解密》(第3版)附录2.这里想举几个的函数的调用例子来说明一下,因为在函数调用时需要特别注意调用的约定,各类调用约定有不同的规矩需要留意,因为一旦使用了汇编,出现问题没有东西可以保护你,唯一可以看到的就是程序崩溃。

对于各类调用约定也可以参考《加密与解密》(第3版),其中的分类还比较详细。

我在《反汇编时的函数识别及各函数调用约定的汇编代码分析》,《C++中通过指针,引用方式做返回值的汇编代码分析》中也有一些实例的分析,但是不能代替完整的说明,仅供参考。

http://blog.csdn.net/vagrxie/archive/2009/01/20/3844768.aspx

http://blog.csdn.net/vagrxie/archive/2009/01/20/3844520.aspx

以下形式就是最方便的内嵌汇编用法:

__asm

{

}

1

// 仅仅返回参数

int __cdecl GetArgument(int ai)

{

int li = 0;

__asm

{

mov eax, ai;

mov li, eax

}

return li;

}

以上程序就是一个仅仅返回参数的函数,用__cdecl调用约定,需要注意的是,mov li, ai的形式是不允许的。。。。与在普通汇编中不允许从内存mov到内存的限制一致。

其实上面的例子中,程序可以优化为如下形式:

2

// 仅仅返回参数

int __cdecl GetArgument(int ai)

{

__asm

{

mov eax, ai

}

}

在函数中没有指定返回值时,eax就是返回值了。。。这和普通的汇编代码一致。。。。顺面看看生成的代码:

printf("%d/n",GetArgument(100));

0041142E push 64h

00411430 call GetArgument (41100Ah)

00411435 add esp,4

足够人性化的,栈由编译器自动的平衡了,不需要我们额外操心,这是很愉快的事情。

再看例三:

// 仅仅返回参数

int __stdcall GetArgument(int ai)

{

__asm

{

mov eax, ai

}

}

这次改成了__stdcall,那么编译器还会帮我们吗?答案还是肯定的。所以也可以放心用。

调用时:

0041142E push 64h

00411430 call GetArgument (4111DBh)

00411435 mov esi,esp

原函数:

// 仅仅返回参数

int __stdcall GetArgument(int ai)

{

004113C0 push ebp

004113C1 mov ebp,esp

004113C3 sub esp,0C0h

004113C9 push ebx

004113CA push esi

004113CB push edi

004113CC lea edi,[ebp-0C0h]

004113D2 mov ecx,30h

004113D7 mov eax,0CCCCCCCCh

004113DC rep stos dword ptr es:[edi]

__asm

{

mov eax, ai

004113DE mov eax,dword ptr [ai]

}

}

004113E1 pop edi

004113E2 pop esi

004113E3 pop ebx

004113E4 add esp,0C0h

004113EA cmp ebp,esp

004113EC call @ILT+330(__RTC_CheckEsp) (41114Fh)

004113F1 mov esp,ebp

004113F3 pop ebp

004113F4 ret 4

其实只关心外部调用者没有维持栈平衡,而函数内部最后的ret 4就够了,完全符合__stdcall的定义。

再接下来呢?thiscall?

int GetArgument(int ai)

{

00411450 push ebp

00411451 mov ebp,esp

00411453 sub esp,0CCh

00411459 push ebx

0041145A push esi

0041145B push edi

0041145C push ecx

0041145D lea edi,[ebp-0CCh]

00411463 mov ecx,33h

00411468 mov eax,0CCCCCCCCh

0041146D rep stos dword ptr es:[edi]

0041146F pop ecx

00411470 mov dword ptr [ebp-8],ecx

__asm

{

mov eax, ai

00411473 mov eax,dword ptr [ai]

add eax, [ecx]

00411476 add eax,dword ptr [ecx]

}

}

00411478 pop edi

00411479 pop esi

0041147A pop ebx

0041147B add esp,0CCh

00411481 cmp ebp,esp

00411483 call @ILT+330(__RTC_CheckEsp) (41114Fh)

00411488 mov esp,ebp

0041148A pop ebp

0041148B ret 4

最后的结果也是正确的,也符合ecxthis指针,并且函数内部维护栈的约定,这个例子也许得将测试程序也贴出来:

int _tmain(int argc, _TCHAR* argv[])

{

CTestThisCall lo(20);

printf("%d/n",lo.GetArgument(10));

system("PAUSE");

return 0;

}

得出的结果是30,结果是正确的。

然后呢?fastcall?

见例4

int __fastcall GetArgument(int ai)

{

__asm

{

mov eax, ecx

}

}

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10));

system("PAUSE");

return 0;

}

结果也是正确的,fastcall的规则在VC中是已知的,但是在很多编译器中实现并不一样,所以很多书籍推荐最好不要使用fastcall来内嵌汇编,因为你不知道到底fastcall到底会使用哪些寄存器来传递参数。

MSDN中有如下描述:

In general, you should not assume that a register will have a given value when an __asm block begins. Register values are not guaranteed to be preserved across separate __asm blocks. If you end a block of inline code and begin another, you cannot rely on the registers in the second block to retain their values from the first block. An __asm block inherits whatever register values result from the normal flow of control.

If you use the __fastcall calling convention, the compiler passes function arguments in registers instead of on the stack. This can create problems in functions with __asm blocks because a function has no way to tell which parameter is in which register. If the function happens to receive a parameter in EAX and immediately stores something else in EAX, the original parameter is lost. In addition, you must preserve the ECX register in any function declared with __fastcall.

To avoid such register conflicts, don't use the __fastcall convention for functions that contain an __asm block. If you specify the __fastcall convention globally with the /Gr compiler option, declare every function containing an __asm block with __cdecl or __stdcall. (The __cdecl attribute tells the compiler to use the C calling convention for that function.) If you are not compiling with /Gr, avoid declaring the function with the __fastcall attribute.

在《加密与解密》中有适当的翻译。。。。我就不翻译了,应该能看懂吧。其实大意就是最好别用fastcall,原因主要是不知道哪个寄存器用来传递参数,假如一个函数中你需要使用ecx一定要记得将其还原。。。呵呵,这个道理很简单,因为一个参数的时候fastcall必定是由ecx来传递参数的。两个参数就是ecx,edx,起码在VS2005中是这样。虽然个人没有感觉用起来有多大的风险,但是文中甚至提及可能通过eax传递参数。。(应该是在优化的时候),所以假如真的用了fastcall,请看汇编代码确认一下,以防万一。

再见一个更详细的例5

int __fastcall GetArgument(int ai1, int ai2, int ai3)

{

__asm

{

mov eax, ecx

add eax, edx

add eax, [ebp+8]

}

}

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10, 20, 30));

system("PAUSE");

return 0;

}

反汇编:

printf("%d/n",GetArgument(10, 20, 30));

00411ACE push 1Eh

00411AD0 mov edx,14h

00411AD5 mov ecx,0Ah

00411ADA call GetArgument (4111F4h)

int __fastcall GetArgument(int ai1, int ai2, int ai3)

{

004130B0 push ebp

004130B1 mov ebp,esp

004130B3 sub esp,0D8h

004130B9 push ebx

004130BA push esi

004130BB push edi

004130BC push ecx

004130BD lea edi,[ebp-0D8h]

004130C3 mov ecx,36h

004130C8 mov eax,0CCCCCCCCh

004130CD rep stos dword ptr es:[edi]

004130CF pop ecx

004130D0 mov dword ptr [ebp-14h],edx

004130D3 mov dword ptr [ebp-8],ecx

__asm

{

mov eax, ecx

004130D6 mov eax,ecx

add eax, edx

004130D8 add eax,edx

add eax, [ebp+8]

004130DA add eax,dword ptr [ai3]

}

}

004130DD pop edi

004130DE pop esi

004130DF pop ebx

004130E0 add esp,0D8h

004130E6 cmp ebp,esp

004130E8 call @ILT+330(__RTC_CheckEsp) (41114Fh)

004130ED mov esp,ebp

004130EF pop ebp

004130F0 ret 4

说实话,ebp+8的计算我原来是搞错了,我当时直接用了esp+8,但是后来才发现在debug没有优化的时候,此处是会采用先保存esp,然后再用ebp的标准用法的。这里没有任何可健壮和移植性可言,仅仅是为了说明thiscall其实也是有规则并且符合约定的,比如上述程序在VS2005最大速度优化时也能正确运行,反汇编代码如下:

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10, 20, 30));

00401010 mov edx,14h

00401015 push 1Eh

00401017 lea ecx,[edx-0Ah] ;注意此处,是编译器相当强程序优化的 成果,用此句替代了mov ecx,0Ah,原因说白了就很简单,因为此句才3个字节,而用mov的需要5个字节。。。。。

0040101A call GetArgument (401000h)

0040101F push eax

00401020 push offset string "%d/n" (4020F4h)

00401025 call dword ptr [__imp__printf (4020A4h)]

system("PAUSE");

0040102B push offset string "PAUSE" (4020F8h)

00401030 call dword ptr [__imp__system (40209Ch)]

00401036 add esp,0Ch

return 0;

00401039 xor eax,eax

}

int __fastcall GetArgument(int ai1, int ai2, int ai3)

{

00401000 55 push ebp

00401001 8B EC mov ebp,esp

__asm

{

mov eax, ecx

00401003 8B C1 mov eax,ecx

add eax, edx

00401005 03 C2 add eax,edx

add eax, [ebp+8]

00401007 03 45 08 add eax,dword ptr [ai3]

}

}

0040100A 5D pop ebp

0040100B C2 04 00 ret 4

最后,在VS2005中还有一个专门为内嵌汇编准备的函数调用方式,naked。。。裸露的调用方式?-_-!呵呵,其实应该表示是无保护的。

6

int __declspec(naked) __stdcall GetArgument(int ai)

{

__asm

{

mov eax, [esp+04H]

ret 4

}

}

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10));

system("PAUSE");

return 0;

}

反汇编代码:

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10));

00401010 push 0Ah

00401012 call GetArgument (401000h)

00401017 push eax

00401018 push offset string "%d/n" (4020F4h)

0040101D call dword ptr [__imp__printf (4020A4h)]

system("PAUSE");

00401023 push offset string "PAUSE" (4020F8h)

00401028 call dword ptr [__imp__system (40209Ch)]

0040102E add esp,0Ch

return 0;

00401031 xor eax,eax

}

int __declspec(naked) __stdcall GetArgument(int ai)

{

__asm

{

mov eax, [esp+04H]

00401000 mov eax,dword ptr [esp+4]

ret 4

00401004 ret 4

}

}

哈哈,在debugnaked函数都是如此简洁,因为我们对其有完完全全的控制,非常完全!以至于。。。在这里你想用mov eax,ai的形式都是不可以的。。。只能完全遵循普通汇编的规则来走。

但是无论此函数再怎么nakedVS也不是完全不管的,见下例7

int __declspec(naked) __cdecl GetArgument(int ai)

{

__asm

{

mov eax, [esp+04H]

ret

}

}

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10));

system("PAUSE");

return 0;

}

假如VS完全不管GetArgument函数,因为GetArgument函数内部无法维护栈平衡,那么程序崩溃是必然的,还好VS管理了这个问题:

printf("%d/n",GetArgument(10));

00411ACE push 0Ah

00411AD0 call GetArgument (4111FEh)

00411AD5 add esp,4

这里需要说明的是,当一个函数naked的时候,完全的指挥权都在程序员手中,包括栈的平衡,函数的返回都需要程序员来做,ret是必不可少的,这里想说明的是,一旦你使用了内嵌汇编,你就开始走在了悬崖边上,因为C++强健的编译时期类型检查完全帮助不了你,能够帮助你的仅仅是你的大脑和你对汇编的理解了。呵呵,既然是用汇编写代码,其实都没有反汇编的概念可言,源代码就在哪儿,看仔细了debug比什么都重要,ollydbg,softICE,windbg你爱用什么就用什么吧。

另外,当函数不是naked的时候,从理论上讲你也是可以在汇编中ret的,只不过,你冒的风险也更大了,因为VS好像还不够智能化,它无法预见你的ret,或者从设计上来讲,不是naked的时候你不应该自己处理ret的,所以,即使是你在内嵌汇编中已经有了ret,它还是会原封不动的用于函数返回的代码。。。。

见下例8

int __cdecl GetArgument(int ai)

{

__asm

{

mov eax, ai

mov esp,ebp

pop ecx

ret

}

}

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10));

system("PAUSE");

return 0;

}

上例在VS2005中最大速度优化,内联选项为仅仅在有inline声明才内联时可以运行正确,见汇编代码:

int _tmain(int argc, _TCHAR* argv[])

{

printf("%d/n",GetArgument(10));

00401020 call GetArgument (401000h)

00401025 push eax

00401026 push offset string "%d/n" (4020F4h)

0040102B call dword ptr [__imp__printf (4020A4h)]

system("PAUSE");

00401031 push offset string "PAUSE" (4020F8h)

00401036 call dword ptr [__imp__system (40209Ch)]

0040103C add esp,0Ch

return 0;

0040103F xor eax,eax

}

int __cdecl GetArgument(int ai)

{

00401000 push ebp

00401001 mov ebp,esp

00401003 push ecx

00401004 mov dword ptr [ai],0Ah

__asm

{

mov eax, ai

0040100B mov eax,dword ptr [ai]

mov esp,ebp

0040100E mov esp,ebp

pop ecx

00401010 pop ecx

ret

00401011 ret

}

}

00401012 mov esp,ebp

00401014 pop ebp

00401015 ret

当你在不应该ret的时候ret,带来的往往是灾难性的后果,所以此处你必须的预见(或者先反汇编看到)原程序会做的所有操作,并且完整的重复它们,不然你在一个非naked的函数中用ret,结果几乎肯定是程序的崩溃。无论如何,除非你有特殊原因,还是按规矩办事比较好。

其实这样的代码对于反汇编来说还挺有意思的:IDA中可以看到

.text:00401000 ; ===========================================================================

.text:00401000

.text:00401000 ; Segment type: Pure code

.text:00401000 ; Segment permissions: Read/Execute

.text:00401000 _text segment para public 'CODE' use32

.text:00401000 assume cs:_text

.text:00401000 ;org 401000h

.text:00401000 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing

.text:00401000

.text:00401000 ; =============== S U B R O U T I N E =======================================

.text:00401000

.text:00401000 ; Attributes: bp-based frame

.text:00401000

.text:00401000 GetArgument proc near ; CODE XREF: _mainp

.text:00401000

.text:00401000 var_4 = dword ptr -4

.text:00401000

.text:00401000 55 push ebp

.text:00401001 8B EC mov ebp, esp

.text:00401003 51 push ecx

.text:00401004 C7 45 FC 0A 00 00 00 mov [ebp+var_4], 0Ah

.text:0040100B 8B 45 FC mov eax, [ebp+var_4]

.text:0040100E 8B E5 mov esp, ebp

.text:00401010 59 pop ecx

.text:00401011 C3 retn

.text:00401011 GetArgument endp

.text:00401011

.text:00401012 ; ---------------------------------------------------------------------------

.text:00401012 8B E5 mov esp, ebp

.text:00401014 5D pop ebp

.text:00401015 C3 retn

多余的代码IDA人性的分割掉了,这在没有源代码的人那里过去了就过去了,要是真深究的话,可能还会以为因为花指令的影响导致代码反汇编错了。。。呵呵,不然也不会有3行代码无缘无故的被分割在那里,一看也不像数据段。。。。。

另外push ecx,pop ecx的处理也是编译器极端优化的一个例子,这里本来可以用sub,add修改esp的做法的,但是那样的话一条指令就是5个字节。。。这样的话才1个字节!真是极端的优化啊。。。。。

参考资料:

1. MSDN

2. 《加密与解密》(第3版)附录2

write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie

分享到:
评论

相关推荐

    在VC2008下将32位C++内嵌汇编迁移到64位

    64位Windows不支持内嵌汇编,在VC2008下将32位C++内嵌汇编迁移到64位,解决64位系统不支持内嵌汇编。

    在C 中内嵌汇编.docx

    在C++中内嵌汇编代码分析 用JAVA或者Python的人常常会告诉你,现在的硬件已经太快了,以至于你可以完全不再考虑性能,快速的开发才是最最重要的。这是个速食者的年代,什么都是,甚至是编程。 C/C++程序员常常会发现...

    C++内嵌汇编加法实现.zip

    C++内嵌汇编x86实现加法,汇编原理实验实现

    C++内嵌汇编(一):反汇编分析C++代码

    NULL 博文链接:https://chuanwang66.iteye.com/blog/1433218

    vs下汇编函数、sse优化、内嵌汇编示例

    这是我自己学习汇编vs下优化时写的一个简单示例,仅供初学者参考,在编译工程时遇到问题,请下看一下Readme1.txt。

    AT&T 汇编语法,以及GCC 的内嵌汇编语法

    讨论AT&T 的汇编语法,以及GCC 的内嵌汇编语法。

    在 Visual C++ 中使用内联汇编.pdf

    在 Visual C++ 中使用内联汇编

    在c/c++代码中嵌入汇编指令

     在C/C++程序中使用内嵌的汇编指令应注意以下事项。  在汇编指令中,逗号(,)用作分隔符。因此如果指令中的C/C++表达式中包含有逗号(,),则该表达式应该被包含在括号中。例如:  其中,(f(),Z)为C/...

    __asm__ __volatile__内嵌汇编用法简述

     __asm__ __volatile__内嵌汇编用法简述 在阅读C/C++原码时经常会遇到内联汇编的情况,下面简要介绍下__asm__ __volatile__内嵌汇编用法。因为我们华清远见教学平台是ARM体系结构的,所以下面的示例都是用ARM汇编。...

    arm内嵌汇编

    arm汇编编程,c语言内嵌汇编编程实例。

    c语言asm汇编内嵌语法.pdf

    GCC 支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作GCC Inline ASM——GCC内联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法表达的指令直接潜入C/C++代码中,另外也允许我们直接写 C/C++代码中...

    EDA/PLD中的__asm__ __volatile__内嵌汇编用法简述

     __asm__ __volatile__内嵌汇编用法简述 在阅读C/C++原码时经常会遇到内联汇编的情况,下面简要介绍下__asm__ __volatile__内嵌汇编用法。因为我们华清远见教学平台是ARM体系结构的,所以下面的示例都是用ARM汇编。...

    使用NASM汇编以及C++(内嵌MASM)编写的实模式操作系统,以及一个汇编编写的QQ堂图形化游戏.zip

    对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】: 有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 鼓励下载和使用,并欢迎大家互相学习,共同...

    c语言内嵌汇编_asm

    c语言与汇编混合编程,用_asm关键字可以实现此功能,在c++编程环境里一样适用,主要用于嵌入式开发。

    EDA/PLD中的在c/c++代码中嵌入汇编指令

     在C/C++程序中使用内嵌的汇编指令应注意以下事项。  在汇编指令中,逗号(,)用作分隔符。因此如果指令中的C/C++表达式中包含有逗号(,),则该表达式应该被包含在括号中。例如:  其中,(f(),Z)为C/...

    Intel汇编语言程序设计 第四版

    ·讲述了高级语言内嵌汇编代码以及汇编语言程序同实地址模式及保护模式的C/C++程序的链接 ·附带扩展的指令集手册,其中包含了指令格式和CPU标志的使用 ·讲述了中断和量和设备I/O ·随书附带的CD-ROM中包含了...

    gcc-inline-assembly

    GCC 内嵌汇编的语法 详细解析,解压之后是一个HTML文件

    Intel汇编语言程序设计(第四版)

    ·讲述了高级语言内嵌汇编代码以及汇编语言程序同实地址模式及保护模式的C/C++程序的链接 ·附带扩展的指令集手册,其中包含了指令格式和CPU标志的使用 ·讲述了中断和量和设备I/O ·随书附带的CD-ROM中包含了...

    AT&T ASM语法

    开发一个OS,尽管绝大部分代码只需要用C/C++等高级语言就可以了,但至少和硬件相关部分的代码需要使用汇编语言,另外,由于启动部分的代码有大小限制,使用精练的汇编可以缩小目标代码的Size。另外,对于某些需要被...

Global site tag (gtag.js) - Google Analytics