因此,当我们的 C 程序(或其他语言)的函数 (funcA) 调用同一程序中的另一个函数 (funcB) 时,funcA 被认为是非叶函数,因为它调用其他函数。因此,设置了堆栈框架和所有内容,而不是使用 redzone。
然而,比如说在 funcB 中,我们不会调用在程序本身中显式编写的任何函数,但我们确实调用了一两个库函数,例如 fscanf()、fopen() (但我认为这并不重要,只要它是库函数)。那么 funcB 是否会不是叶函数,因为它仍在调用另一个函数?x86 中如何处理库函数?
分析一些 x86 很明显没有发生明显的跳转,但我可以看到它的执行方式类似于,call __isoc99_fscanf@PLT #和call perror@PLT #。
不,库函数并不特殊,除了一些类似的函数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 玩具函数库进行手写汇编,其中所有寄存器都是调用保留的,除了如果有返回值则返回。)
事实上,恰恰相反:调用在同一源文件中定义的函数可以让编译器内联它(如果它选择),使调用者成为叶子。
#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(),但这只是一条额外的指令。
| 归档时间: |
|
| 查看次数: |
115 次 |
| 最近记录: |