调用库函数是否仍然会使它们成为非叶函数?x86 汇编如何处理库函数?

dyn*_*der 0 c x86 assembly

因此,当我们的 C 程序(或其他语言)的函数 (funcA) 调用同一程序中的另一个函数 (funcB) 时,funcA 被认为是非叶函数,因为它调用其他函数。因此,设置了堆栈框架和所有内容,而不是使用 redzone。

然而,比如说在 funcB 中,我们不会调用在程序本身中显式编写的任何函数,但我们确实调用了一两个库函数,例如 fscanf()、fopen() (但我认为这并不重要,只要它是库函数)。那么 funcB 是否会不是叶函数,因为它仍在调用另一个函数?x86 中如何处理库函数?

分析一些 x86 很明显没有发生明显的跳转,但我可以看到它的执行方式类似于,call __isoc99_fscanf@PLT #call perror@PLT #

Pet*_*des 6

不,库函数并不特殊,除了一些类似的函数memcpy可以内联。 在生成的 asm 中,如果有一条call指令,则该 asm 函数是非叶函数。 如果不是,它是一个叶函数(即使它以使用jmp另一个函数的优化尾部调用结束)。

请注意,由于内联(包括编译器知道的一些库函数,例如简单的数学和小常量大小的 string/memcpy),以及优化对结果未使用的“纯”函数的调用,C 函数使得调用仍可能优​​化为作为叶子的 asm 函数。递归有时也可以优化为迭代。

在另一个方向上,编译器可以将循环优化为对 memset 或 memcpy 的调用,例如类似的循环 for (size_t i=0 ; i<n ; i++) arr[i] = 0;。即使 C 源代码没有任何调用,这也可以生成非叶 asm 函数。


叶与非叶在汇编中很重要,因为您必须在另一个调用之前重新对齐堆栈,并且像 ECX 这样的调用破坏寄存器中的内容会被任何函数调用破坏。您提到了红色区域:如果某个函数(或可能在某些执行路径上)需要跨函数调用生存,则无法将任何内容保留在 RSP 以下的红色区域中,或者由函数调用编写为一个输出。

库函数遵循与编译器现在生成的调用约定相同的调用约定,因此调用库函数不会放松任何这些要求。(在 Windows 上有多个 32 位调用约定,但它们都具有相同的一组调用破坏寄存器。除非您使用 Irvine32 玩具函数库进行手写汇编,其中所有寄存器都是调用保留的,除了如果有返回值则返回。)

事实上,恰恰相反:调用在同一源文件中定义的函数可以让编译器内联它(如果它选择),使调用者成为叶子。


示例(关于Godbolt

#include <stdlib.h>
#include <string.h>

int bar(int,int);

int leaf(int *p, int a){
    *p = 0;
    int c = (a<10);
    memcpy(&a, &c, sizeof(a));   // defined by GCC as __builtin_memcpy, optimizes away to a=c
    *p = c;
    return bar(a, a);           // tailcall
}
Run Code Online (Sandbox Code Playgroud)

对于 x86-64,使用 GCC11.2 -O3 进行编译相当简单

leaf:
        mov     r8, rdi            # silly compiler, could have avoided this by materializing the boolean into ESI, leaving EDI untouched until after the store
        xor     edi, edi           # zero a register to setcc into to get a zero-extended 0/1
        cmp     esi, 9
        setle   dil                # EDI = (a<=9) // (a<10)
        mov     DWORD PTR [r8], edi  # store to the pointer.  The store of 0 earlier is optimized out as a dead store
        mov     esi, edi           # copy the arg for bar(a,a)
        jmp     bar
Run Code Online (Sandbox Code Playgroud)

请注意,*p = 0;存储已被优化(消除死存储),因为我们将其他内容存储到同一位置。与下面的函数不同,我们不会调用任何可能读取某些全局变量(可能p指向)的代码。尽管许多数学库函数都是这样,但大多数库函数并未被编译器特殊处理为不触及任何全局状态。因此,当对编译器一无所知的非内联函数进行函数调用时,编译器必须使所有内存(非转义局部变量除外)与 C 抽象机同步(对优化器“不透明”)。这包括此编译单元中未定义的所有函数,除非您使用链接时优化来允许跨文件过程间分析/优化和内联。

非叶函数在 C 中看起来没有太大不同,但我选择了编译器不会内联的 C 库函数。我什至省略了这a<10部分,但它仍然更像是汇编。

int non_leaf(int *p, int a){
    *p = 0;
    int c = rand();
    *p = c;
    return bar(a, a);
}
Run Code Online (Sandbox Code Playgroud)
non_leaf:
        push    rbp                  # save a call-preserved reg
        mov     ebp, esi             # use it to save a for use after rand
        push    rbx
        mov     rbx, rdi             # and another for the pointer
        sub     rsp, 8               # realign the stack by 16
   # end of function prologue
        mov     DWORD PTR [rdi], 0   # *p = 0;   not optimized away because GCC doesn't know that p won't be pointing into memory that rand() reads or writes
        call    rand                 # int c = rand()
        mov     esi, ebp
        mov     edi, ebp             # set up both args for the tailcall to bar
        mov     DWORD PTR [rbx], eax # store *p = c
   # start of function epilogue
        add     rsp, 8               # epilogue: restore stack stuff back to function-entry state
        pop     rbx
        pop     rbp
        jmp     bar                  # tailcall with edi=esi = incoming ESI
Run Code Online (Sandbox Code Playgroud)

请注意,这是唯一一个必须对堆栈或任何其他类型的序言/尾声执行任何操作的版本。在叶函数中,如果复杂函数用完了调用破坏的寄存器,则只能推送/弹出调用保留的寄存器。

调用您在另一个文件中定义的函数看起来完全相同。(除非您在同一 gcc 命令行上使用-flto, 或-fwhole-program两个文件)

但是调用此文件中定义的函数以使其可以内联是不同的:

int helper(int a){
    return a+1;
}
int call_inline(int *p, int a){
    *p = 0;
    int c = helper(a);
    *p = c;
    return c;
}
Run Code Online (Sandbox Code Playgroud)
# Not declared static, so GCC emits a stand-alone definition in case another file wants to call it.
helper:
        lea     eax, [rdi+1]
        ret

# But the call from this function fully inlined:
call_inline:
        lea     eax, [rsi+1]              # c(eax) = helper(a) = a+1
        mov     DWORD PTR [rdi], eax      # *p = c;
        ret                               # return c (still in EAX)
Run Code Online (Sandbox Code Playgroud)

我还删除了对 的尾调用bar(),但这只是一条额外的指令。