科技行者

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

知识库

知识库 安全导航

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

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

  • 扫一扫
    分享文章到微信

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

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

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

关键字: ATL 布幔 底层技术 汇编

  • 评论
  • 分享微博
  • 分享邮件
介绍

  到现在为止,我们还没有讨论过任何有关汇编语言的东西。但是如果我们真的要了解ATL底层内幕的话,就不能回避这一话题,因为ATL使用了一些底层的技术以及一些内联汇编语言来使它更小巧快速。在这里,我假设读者已经拥有了汇编语言的基础知识,所以我只会集中于我的主题,而不会再另外写一份汇编语言的教程。如果你尚未足够了解汇编语言,那么我建议你看一看Matt Pietrek于1998年2月发表在Microsoft System Journal的文章《Under The Hood》,这篇文章会给予你关于汇编语言足够的信息的。

   现在就要开始我们的旅行了,那么先以这个简单的程序作为热身吧:

  程序55.

void fun(int, int) {
}
int main() {
  fun(5, 10);
  return 0;
}

  在在命令行模式下,使用命令行编译器cl.exe来编译它。在编译的时候,使用-FAs开关,例如,如果程序的名字是prog55的话:  
Cl -FAs prog55.cpp
  这就会生成一个带有相同文件名,扩展名为.asm的文件,这个文件中包含有以下程序的汇编语言代码。现在看看生成的输出文件,让我们首先来讨论函数的调用吧。调用函数的汇编代码是类似这个样子:

push  10            ; 0000000aH
push  5
call  ?fun@@YAXHH@Z ; fun

  首先,函数的参数以自右而左的顺序入栈,然后再调用函数。但是,函数的名称和我们给定的有所不同,这是由于C++编译器会对函数的名称作一些修饰已完成函数的重载。让我们稍微修改一下程序,重载这个函数,再来看看代码的行为吧。

  程序56.

void fun(int, int) {
}
void fun(int, int, int) {
}
int main() {
  fun(5, 10);
  fun(5, 10, 15);
  return 0;

  现在调用这两个函数的汇编代码是类似这个样子:

push  10             ; 0000000aH
push  5
call  ?fun@@YAXHH@Z  ; fun
push  15             ; 0000000fH
push  10             ; 0000000aH
push  5
call  ?fun@@YAXHHH@Z ; fun

  请看函数的名字,我们编写了两个名称相同的函数,但是编译器将函数名做了修饰完成了函数重载的工作。

  如果你不希望修饰函数的名称,那么你可以对函数使用extern "C"。让我们来对程序作少许修改。

  程序57.

extern "C" void fun(int, int) {
}
int main() {
  fun(5, 10);
  return 0;
}

  调用函数的汇编代码为

push  10   ; 0000000aH
push  5
call  _fun

  这就意味着现在你就不能对这个带有C链接方式的函数进行重载了。请看以下的程序

  程序58.

extern "C" void fun(int, int) {
}
extern "C" void fun(int, int, int) {
}
int main() {
  fun(5, 10);
  return 0;
}

  这个程序会给出一个编译错误,因为函数的重载在C语言中是不支持的,并且你给两个函数起同样的名称的同时还告诉编译器不要修饰它的名字,也就是使用C的链接方式,而不是C++的链接方式。

  现在来看看编译器为我们那个什么也不做的函数生成了什么,下面是编译器为我们的函数生成的代码。
push  ebp
mov   ebp, esp
pop   ebp
ret   0

  在我们进行详细地讲解之前,请看以下函数的最后一条语句,也就是ret 0。为什么是0?或者可以是别的非0数吗?正如我们所见,我们向函数传递的所有参数事实上都被压入了堆栈。在你或者编译器向堆栈中压入数据的时候,会对寄存器有什么影响吗?请看以下这个简单的程序来观察这一行为吧。我使用了printf而不是cout,这是为了避免cout的开销。

  程序59.
#include <cstdio>
int g_iTemp;
int main() {
  fun(5, 10); // 译注:这里的fun,应该是上文中的void fun(int, int)
  _asm mov g_iTemp, esp
  printf("Before push %d\n", g_iTemp);
  _asm push eax
  _asm mov g_iTemp, esp
  printf("After push %d\n", g_iTemp);
  _asm pop eax
  return 0;
}

  程序的输出为:
Before push 1244980
After push 1244976
  这个程序显示了压栈前后ESP寄存器中的值。下图清楚地说明了在你向堆栈中压入数据后,ESP的值会减少。


  现在就有一个问题了,当我们向函数中传递参数的时候,是谁来负责恢复堆栈指针的呢——函数本身还是函数的调用者?事实上,这两种情况都有可能,并且这就是标准调用约定和C调用约定的不同。请看调用函数的下一条语句:
push  10     ; 0000000aH
push  5
call  _fun
add   esp, 8
  在这里有两个参数传递给了函数,所以堆栈指针在两个参数入栈后会减去8个字节。现在在这个程序中,设置堆栈指针就是函数调用者的职责了。这就称作C调用约定。在这种调用约定中,你可以传递可变数目的参数,因为调用者知道有多少参数传递给了函数,所以它可以来设置堆栈指针。

  然而,如果你选择了标准调用约定,那么清楚堆栈就是被调用者的工作了。所以在这种情况下,可变数目的参数是不能传递给函数的,因为那样的话函数就没有办法知道到底有多少参数传给了函数,也就无法正常设置堆栈指针了。

  请看下面的程序来观察标准调用约定的行为。

  程序60.

extern "C" void _stdcall fun(int, int) {
}
int main() {
  fun(5, 10);
  return 0;
}

  现在来看看函数的调用。

push  10     ; 0000000aH
push  5
call  _fun@8

  在这里,函数名称中的@表示这是一个标准调用约定,8则表示被压入堆栈的字节数。所以,参数的数目可以由这个数目除以4得知。

  以下是我们这个什么也不做的函数的代码:

push  ebp
mov   ebp, esp
pop   ebp
ret   8

  这个函数通过“ret 8”指令在返回之前设置了堆栈指针。

  现在来探究一下编译器为我们产生的代码。编译器插入这个代码来创建堆栈帧,这样它就可以通过标准方式来存取参数和局部变量了。堆栈帧是一个为函数保留的区域,用来存储关于参数、局部变量和返回地址的信息。堆栈帧通常是在新的函数调用的时候创建,并在函数返回的时候销毁。在8086体系中,EBP寄存器就被用于存储堆栈帧的地址,有时叫做栈指针。(译注:ESP和EBP在本文中都被作者笼统地称为“Stack Pointer”,事实上ESP应称作“堆栈指针[Stack Pointer]寄存器”,它指示堆栈的栈顶便宜地址;EBP应称作“基址指针[Base Pointer]寄存器”,它用来作为基地址并和偏移量组合使用来访问堆栈中的信息。)

  这样,编译器首先保存前一个堆栈帧的地址,然后使用ESP的值创建新的堆栈帧。函数返回之前,先前的堆栈帧就恢复了。
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

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

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