首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >调用库函数是否仍然是非叶函数?x86程序集如何处理库函数?

调用库函数是否仍然是非叶函数?x86程序集如何处理库函数?
EN

Stack Overflow用户
提问于 2022-04-12 02:46:37
回答 1查看 73关注 0票数 0

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

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

通过分析一些x86,可以清楚地看到没有明显的跳跃发生,但我可以看到它正在执行,比如call __isoc99_fscanf@PLT #call perror@PLT #

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-04-12 04:19:43

不,库函数并不特殊,只有少数像memcpy这样的函数可以内联。在结果的call 中,如果有指令,则asm函数是非叶函数。如果不是,则是叶函数(即使它以使用jmp对另一个函数的优化尾调用结束)。

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

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

在程序集中,叶对非叶的问题,因为您必须在另一个调用之前重新对齐堆栈,并且调用失败寄存器(如ECX )中的内容会受到任何函数调用的打击。您还提到了红色区域:一个函数,如果它需要跨函数调用存在,或者由函数调用作为输出写入,那么它确实(或可能在某些执行路径上)不能将红色区域中的任何东西保存在RSP以下。

库函数与编译器现在生成的函数遵循相同的调用约定,因此调用库函数不会放松这些需求()。(在Windows上有多个32位的调用约定,但它们都有相同的呼叫失败寄存器集。除非您正在为手写的asm使用玩具函数的Irvine32库,否则所有寄存器都会被调用保存,如果有返回值,则除外。)

事实上,恰恰相反:调用在同一个源文件中定义的函数可以让编译器在它选择的情况下内联,从而使调用者成为一个叶。

示例(关于哥德波特 )

代码语言:javascript
复制
#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
}

这非常简单地使用x86 11.2 -O3为x86-64编译。

代码语言:javascript
复制
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

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

非叶函数在C中看起来没有多大不同,但我选择了编译器不内联的C库函数。我甚至忽略了a<10部分,但它仍然是更多的asm。

代码语言:javascript
复制
int non_leaf(int *p, int a){
    *p = 0;
    int c = rand();
    *p = c;
    return bar(a, a);
}
代码语言:javascript
复制
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

请注意,这是唯一必须对堆栈或任何其他类型的序言/结尾进行任何操作的版本。在叶函数中,只有在对复杂函数进行呼叫中断寄存器用完了时才会推送/弹出保持调用的寄存器。

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

但是,调用这个文件中定义的函数以便它可以内联是不同的:

代码语言:javascript
复制
int helper(int a){
    return a+1;
}
int call_inline(int *p, int a){
    *p = 0;
    int c = helper(a);
    *p = c;
    return c;
}
代码语言:javascript
复制
# 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)

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

票数 6
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71836448

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档