科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件ATL布幔下的秘密之底层技术和汇编

ATL布幔下的秘密之底层技术和汇编

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

到现在为止,我们还没有讨论过任何有关汇编语言的东西。但是如果我们真的要了解ATL底层内幕的话

作者:李马编译 来源:VCKBASE 2007年10月19日

关键字:

  • 评论
  • 分享微博
  • 分享邮件
现在来看看堆栈帧中都有什么。在EBP的高地址一边存放所有参数,EBP的低地址一边则存放所有的局部变量。

  函数的返回地址保存在EBP中,前一个堆栈帧的地址保存在EBP + 4。现在看看下面的例子,它拥有两个参数和三个局部变量。

  程序61.

extern "C" void fun(int a, int b) {
  int x = a;
  int y = b;
  int z = x + y;
  return;
}
int main() {
  fun(5, 10);
  return 0;
}

  现在来看看编译器产生的函数代码。

push  ebp
mov   ebp, esp
sub   esp, 12                 ; 0000000cH
; int x = a;
mov   eax, DWORD PTR _a$[ebp]
mov   DWORD PTR _x$[ebp], eax
; int y = b;
mov   ecx, DWORD PTR _b$[ebp]
mov   DWORD PTR _y$[ebp], ecx
; int z = x + y;
mov   edx, DWORD PTR _x$[ebp]
add   edx, DWORD PTR _y$[ebp]
mov   DWORD PTR _z$[ebp], edx
mov   esp, ebp
pop   ebp
ret   0

  现在来看看_x、_y这些东西都是什么。也就是定义在函数定义上方的这些东西:

_a$ = 8
_b$ = 12
_x$ = -4
_y$ = -8
_z$ = -12

  这就意味着你可以像这样阅读代码:

; int x = a;
mov  eax, DWORD PTR [ebp + 8]
mov  DWORD PTR [ebp - 4], eax
; int y = b;
mov  ecx, DWORD PTR [ebp + 12]
mov  DWORD PTR [ebp - 8], ecx
; int z = x + y;
mov  edx, DWORD PTR [ebp - 4]
add  edx, DWORD PTR [ebp - 8]
mov  DWORD PTR [ebp - 12], edx

  这也就意味着参数a和b的地址分别为EBP + 8和EBP + 12。并且,x、y和z的值分别存储在内存中EBP - 4、EBP - 8、EBP - 12的位置上。

  在用这一知识武装起来之后,现在我们来玩一个函数参数的游戏,看以下这个简单的程序:

  程序62.

#include <cstdio>
extern "C" int fun(int a, int b) {
  return a + b;
}
int main() {
  printf("%d\n", fun(4, 5));
  return 0;
}

  就像我们所期望的那样,程序的输出为9。现在让我们来对程序作少许修改。

  程序63.

#include <cstdio>
extern "C" int fun(int a, int b) {
  _asm mov dword ptr[ebp+12], 15
  _asm mov dword ptr[ebp+8], 14
  return a + b;
}
int main() {
  printf("%d\n", fun(4, 5));
  return 0;
}

  程序的输出为29。我们知道参数的地址,并且在程序中我们改变了参数的值。因而,在我们将两个变量相加时,新的变量值15和14就被加起来了。

  VC中函数拥有naked属性。如果你将任何函数指定为naked,那么它就不会为该函数产生prolog代码和epilog代码。那么什么是prolog代码和epilog代码呢?prolog是一个英文词汇,意思是“Opening(开始的)”,当然它也是一种用于AI的程序设计语言的名称——但是在这里,这门语言和编译器产生的prolog代码没有任何关系。prolog代码是编译器自动产生的,它会被插入到函数的开始处来设置堆栈帧。你可以看看程序61产生的汇编语言代码,在函数的开头处,编译器会自动插入以下的代码来设置堆栈帧。

push  ebp
mov   ebp, esp
sub   esp, 12  ; 0000000cH

  这段代码就称作prolog代码。同样,插入在函数末尾的代码就称作epilog代码。在程序61中,编译器生成的epilog代码为:

mov   esp, ebp
pop   ebp
ret   0
  在来看看带有naked属性的函数。 
  程序64.
extern "C" void _declspec(naked) fun() {
  _asm ret
}
int main() {
  fun();
  return 0;
}

  编译器生成的fun函数代码是类似于这个样子:

_asm ret

  这就意味着在这个函数中没有prolog代码和epilog代码。事实上,naked函数有一些规则,也就是你不能在naked函数中定义自动变量。因为如果你这么做的话,编译器就需要为你产生代码,而naked函数中编译器是不会产生任何代码的。其实,你还需要自己编写ret语句,否则程序就会崩溃。你甚至不能在naked函数中编写return语句。为什么呢?因为当你从函数中返回一些东西的时候,编译器就会把它的值放在eax寄存器之中。所以这就意味着编译器会为你的return语句产生代码。让我们通过下面的简单程序来弄懂函数返回值的工作过程吧。
 
  程序64.

#include <cstdio>
extern "C" int sum(int a, int b) {
  return a + b;
}
int main() {
  int iRetVal;
  sum(3, 7);
  _asm mov iRetVal, eax
  printf("%d\n", iRetVal);
  return 0;
}

  程序的输出为10。在这里我们并没有直接使用函数的返回值,而是在函数调用结束后将eax的值复制了一份。

  现在来编写我们的naked函数,这个函数没有prolog代码和epilog代码,它返回了两个变量的和。

  程序65.

#include <cstdio>
extern "C" int _declspec(naked) sum(int a, int b) {
  // prolog代码
  _asm push ebp
  _asm mov ebp, esp
  // 用于相加变量和返回的代码
  _asm mov eax, dword ptr [ebp + 8]
  _asm add eax, dword ptr [ebp + 12]
  
  // epilog代码
  _asm pop ebp
  _asm ret
}
int main() {
  int iRetVal;
  sum(3, 7);
  _asm mov iRetVal, eax
  printf("%d\n", iRetVal);
  return 0;
}

  程序的输出为10,也就是两个参数3和7的和。

  这一属性被用于ATLBASE.H中来实现_QIThunk结构的成员。这个结构被用于在定义了_ATL_DEBUG_INTERFACES的情况下调试ATL程序的引用计数。

  我希望在下一篇文章中能够探究一些ATL的其它秘密。
 

查看本文来源

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章