【问题标题】:Building .so with recursive function in it构建带有递归函数的.so
【发布时间】:2018-08-28 14:02:33
【问题描述】:

在处理某个项目期间,我遇到了无法构建这样的库的问题。我得到了类似的错误:relocation R_X86_64_PC32 against symbol '' can not be used when making a shared object;用 -fPIC 重新编译 最终我设法找到了根本原因。它是库中的递归函数。例如,我有以下众所周知的例子:

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

    mov 16(%rbp),%rax
    cmp $1,%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 $1,%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 库。


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

【问题讨论】:

  • 对于全局符号,您需要使用PLT或GOT,即call factorial@PLTcall *factorial@GOTPCREL(%rip)。如果您愿意,当然可以反向进行包装,这样您就可以保留公共 factorial 符号并使用一些本地进行递归。

标签: assembly shared-libraries x86-64 gnu dynamic-linking


【解决方案1】:

factorial 是一个全局标签,因此可以进行符号插入。见Sorry state of dynamic libraries on Linux。 (还有an example of interposing malloc with LD_PRELOAD,还有一些docs)。

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

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

如果需要,您可以创建一个更简单的“帮助程序”函数,该函数使用自己的自定义调用约定,并且不会为递归部分创建带有 %rbp 的堆栈帧。 (但是在堆栈上获取 arg 已经不是 x86-64 的标准)。


可以通过 PLT 调用或通过 GOT 间接调用内存,但不要这样做;您不希望每个call 的额外开销,并且您不希望符号插入将您的非标准调用约定实现替换为通过%rdi 中的第一个整数arg 的普通实现。

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

您没有在 call 之前维护 16 字节堆栈对齐,但在调用您自己不关心的私有函数时,您不需要这样做。

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


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

【讨论】:

  • 附带说明,GCC8 应该知道优化递归调用以不使用 PLT(感谢PR56727)。
  • 非常感谢,现在清楚了。阶乘只是一个例子。您还说大约 16 字节堆栈对齐。我以为在 x86-64 上应该是 8 字节对齐,不是吗?
  • @AndrewBolotov:如果这回答了您的问题,请单击向上/向下投票箭头下的“接受”复选标记。 :)
猜你喜欢
  • 2013-01-12
  • 1970-01-01
  • 2013-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-25
相关资源
最近更新 更多