在其中构建具有递归函数的 .so

Building .so with recursive function in it

在做一些项目时,我遇到了无法构建 so 库的问题。我收到如下错误:relocation R_X86_64_PC32 against symbol '' cannot be used when make a shared object;使用 -fPIC 重新编译 最终我设法找到了根本原因。它是库中的递归函数。例如,我有以下众所周知的例子:

.section .text
.globl factorial
.type  factorial,STT_FUNC
factorial:
    push %rbp
    mov %rsp,%rbp

    mov 16(%rbp),%rax
    cmp ,%rax
    je end_factorial
    dec %rax
    push %rax  #this is how we pass the argument to function
    call factorial
    pop %rbx
    inc %rbx
    imul %rbx,%rax
end_factorial:
    mov %rbp, %rsp
    pop %rbp
    ret

现在,让我们尝试构建共享库:

as  -g -o fact.o fact.s
ld -shared fact.o -o libfact.so
ld: fact.o: relocation R_X86_64_PC32 against symbol `factorial' can not be used when making a shared object; recompile with -fPIC

如果我包装阶乘函数,像这样:

.section .text
.globl fact
.type  fact,STT_FUNC
fact:
factorial:
    push %rbp
    mov %rsp,%rbp

    mov 16(%rbp),%rax
    cmp ,%rax
    je end_factorial
    dec %rax
    push %rax  #this is how we pass the argument to function
    call factorial
    pop %rbx
    inc %rbx
    imul %rbx,%rax
end_factorial:
    mov %rbp, %rsp
    pop %rbp
    ret

我可以毫无错误地构建 so 库。


问题是:为什么在构建包含递归函数的共享库时出错? P.S。在这种情况下,静态链接工作正常。 谢谢!

factorial是一个全局标签,所以可以进行符号插入。参见 Sorry state of dynamic libraries on Linux. (Also, an example of interposing malloc with LD_PRELOAD, and some docs)。

制作共享库时,call factorial指令的目标不假定为同一文件中定义的factorial:标签。那是 因为 你使用了 .globl factorial.

正如 Jester 指出的那样,您应该为 call 目标定义一个单独的本地标签,这样您就可以保留全局 factorial 名称。

如果需要,您可以创建一个更简单的 "helper" 函数,该函数使用自己的自定义调用约定,并且不会为递归部分创建具有 %rbp 的堆栈帧。 (但是对于 x86-64,在堆栈上取一个 arg 已经是 non-standard)。


可以通过PLT调用或memory-indirect通过GOT调用,但不要那样做;您不希望每个 call 都有额外的开销,并且您不希望符号插入将您的 non-standard-calling-convention 实现替换为传递 [=] 中第一个整数 arg 的普通实现19=].

说起来,在堆栈上传递 arg 很慢。你确实需要 save/restore 东西,除非你 rewrite the recursion to be tail-recursive, like factorial_helper(accumulator*n, n-1)。但是你也不需要每次都用%rbp制作一个堆栈框架。

您没有在 call 之前保持 16 字节堆栈对齐,但是在调用您自己的不关心它的私有函数时不需要它。

当然,如果您完全关心性能,您不会首先使用递归实现,因为 factorial 这样做的唯一原因是作为学习练习。重写 tail-recursive 允许您 (or the compiler if writing in C) 将 call / ret 变成 jmp,这当然会成为一个循环。


相关:What are good examples that actually motivate the study of recursion?。二叉树遍历或 Ackermann 函数递归比迭代更容易实现,但 factorial 或 Fibonacci 更难(在 Fibonacci 的情况下, much 更慢)。