0

在大多数情况下,编译过程是一堆处理器指令,它们在代码段中占据连续的字节范围。它当然可能包含有条件的和无条件的跳转以及非线性执行流程,但是查看反汇编列表,您可以明确地说出程序的开始(即入口点)在哪里以及程序在哪里结束。

但是,有时 CL 将过程拆分为多个部分并将这些部分混合在一起,以便您可以在proc_a的前半部分和proc_a的后半部分之间获得proc_b

问题是:什么命令行开关使编译器生成如下所述的代码。

我正在我的调试器/反汇编器中分析一个可执行文件,并注意到大量具有碎片化主体的函数。我有二进制文件本身,它的调试符号,我知道它是使用 CL 编译的,但我没有源代码,makefile,因此我不知道使用什么命令行选项来编译它。

让我给你看一个小例子(它只是一个演示代码,不是来自现实生活)。

假设您有以下用 C++ 编写的函数(boo是类,方法是virtual):

int foo(boo *x)
{
    if(x->ready == 0)
    {
        return 0;
    }
    else
    {
        x->func_a(x->bzz);
        x->func_b(x->kee);
        return x->func_c();
    }
}

现在正在使用一些神秘的命令行选项运行,CL 决定取一小部分指令(表示return 0;条件分支)并将其移向远离 foo 过程基本部分边界的代码段末尾。此外,这小部分指令将在调试符号表中有自己的条目,其名称由被拆分的过程名称、下划线字符和十进制数组成(在大多数情况下,但不是在所有情况下)跳转指令中相对于过程入口点(例如 foo_13)的跳转目标的偏移量(它本身就是偏移量)。

因此,CL编译如下:

foo:
  push ebp
  mov ebp, esp
  push edi
  mov edi, [esp+8]
  cmp [edi+4], 0
  je foo_X   <----- jump down below to the isolated (!) piece of 'foo'
  push esi
  mov esi, [edi]
  mov ecx, edi
  push [edi+8]
  call [esi]
  push [edi+12d]
  mov ecx, edi
  call [esi+4]
  mov ecx, edi
  call [esi+8]
  pop esi
  pop edi <---- return from small piece 'foo_X' leads here
  pop ebp
  retn 4

OtherFunc1:
  <code for other function>
  <code for other function>   
  <code for other function>
OtherFunc2:
  <code for other function>
  <code for other function>   
  <code for other function>

  <many many code not related to 'foo' at all>

foo_X:
  xor eax, eax
  jmp <address of 'pop edi' within main part of 'foo'>

foo_X(X 代表如上所述的某个十进制数)是一个小的两指令块,表示 if 语句的真分支。

就我而言,有一大堆这样的小块。它们中的大多数(但不是全部)是两条指令的(通过重置某些寄存器xor reg, reg并跳回函数的主要部分,或者将 EAX 归零并执行 RETN)。它们在调试符号表中都有自己的名称。如果我们有foobarbaaz 之类的函数,那么还有foo_7bar_22bar_43baaz_19。大多数这些小块被组合在一起,并在代码部分中彼此靠近,但远离它们的对应部分(foo,bar,baaz),因此到这些块的跳转遍及代码部分。


它可能与基于分支预测的优化有关:编译器将它认为不太可能发生的执行分支移离基本执行流路径。但是,我在 1998 年编译的二进制文件中观察到了这些技巧,因此很明显,使用了来自 MSVC6(甚至是 MSVC5!)的 CL.EXE,并且没有办法向那些旧版本的优化器提供有关分支预测的提示CL。是的,现代版本的 CL 支持profile-guided optimization,但我们谈论的是 1998 年编译的代码。

我正在寻找的选项之一是/Gy - 一个所谓的功能级链接选项。此选项指示编译器将每个单独的函数包装到单独的 COMDAT 中,以便在链接时可以以任何所需的顺序重新排序函数,并且可以排除其中的一些函数。但是,据我所见,它将整个函数包装到单个 COMDAT 中,但就我而言,它需要将单个函数的单独片段放入单独的 COMDAT 中,以 (1) 允许链接器将函数片段放置在很远的地方彼此和(2)允许这样的小片段在调试符号表中拥有自己的名称。

再一次,我的问题是 CL/LINK 的哪些命令行选项/开关控制这种行为。

4

0 回答 0