【问题标题】:How do I link a C++ subroutine to an x86 assembly program?如何将 C++ 子例程链接到 x86 汇编程序?
【发布时间】:2023-12-23 10:36:01
【问题描述】:

我正在尝试制作一个打印“Hello!”的简单汇编程序。一次,等待一秒钟,然后再次打印。由于 sleep 函数在汇编中相对复杂,而且我不太擅长,所以我决定使用 C++ 来制作 Sleep 子例程。这是 C++ 程序:

// Sleep.cpp
#include <thread>
#include <chrono>

void Sleep(int TimeMs) {
    std::this_thread::sleep_for(std::chrono::milliseconds(TimeMs));
}

然后我使用“gcc -S Sleep.cpp”将此睡眠函数编译为汇编程序,然后使用“gcc -c Sleep.s”将其编译为目标文件

我正在尝试从程序集中调用这个 C++ 子例程。我听说您通过将 C++ 子例程推入堆栈来为它们提供参数,这是我目前的汇编代码:

        global    _main
        extern    _puts
        extern    Sleep
        section   .text
_main:    
        push    rbp
        mov     rbp,    rsp
        sub     rsp,    32


        ;Prompt user:
        lea     rdi,    [rel prompt]        ; First argument is address of message
        call    _puts                       ; puts(message)

        push    1000 ; Wait 1 second (Sleep time is in milliseconds)
        call    Sleep

        lea     rdi,    [rel prompt] ; Print hello again
        call    _puts

        xor     rax,    rax                 ; Return 0
        leave
        ret

        section   .data

prompt:
    db      "Hello!", 0

这两个文件都保存到桌面/程序。我正在尝试使用 NASM 和 GCC 编译它,我的编译器调用是:

nasm -f macho64 Program.asm && gcc Program.o Sleep.s -o Program && ./Program

但我得到了错误:

"Sleep", referenced from:
      _main in Program.o
     (maybe you meant: __Z5Sleepi)
  "std::__1::this_thread::sleep_for(std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000000l> > const&)", referenced from:
      void std::__1::this_thread::sleep_for<long long, std::__1::ratio<1l, 1000l> >(std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000l> > const&) in Sleep-7749e0.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

将代码更改为“extern __Z5Sleepi”并调用“__Z5Sleepi”而不是 Sleep 似乎无法解决问题。 (我收到相同的错误消息,只是没有“也许你的意思是 __Z5Sleepi”位。我还尝试使用 _Sleep 而不是 Sleep,但没有成功。)我做错了什么?如何正确使用此 C++ 子例程并将其与我的汇编程序链接?到目前为止,我使用的方法是否完全错误?

非常感谢任何帮助,浏览堆栈溢出,似乎有很多关于此的问题,但实际上没有一个问题进入链接过程。 (而且他们似乎在询问将程序集与 C++ 链接,而不是 C++ 与程序集。)我正在使用 NASM 和 GCC 进行编译,我的平台是 Mac OSX。

【问题讨论】:

  • 首先,使用g++进行链接。要摆脱名称混淆,请在您的 c++ 源代码中使用 extern C
  • @Jester 抓紧最后一条评论。我的 asm 文件仍在使用 __Z5Sleepi 而不是 Sleep。现在,使用 _Sleep 程序编译并执行。除了现在,当它应该暂停 1 秒时,我得到一个分段错误。知道为什么会这样吗?
  • 你可能知道(因为你正确地将它用于puts)你必须把参数放在rdi而不是堆栈上。因此push 1000 应该是mov edi, 1000
  • arg-passing 部分是What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 的副本。在某处也可能存在名称修改部分的副本。是的,extern "C"How to call C++ functions in my assembly code
  • 即使你得到了这个工作,我强烈建议你用 C++ 编写你的 main 函数并让它调用你的汇编函数。也使用 g++ 链接,@Jester 也推荐。这将确保正确初始化 C++ 运行时库。

标签: c++ gcc assembly x86 nasm


【解决方案1】:

正如 Jester 所指出的,问题源于两件事。一个是我需要更改 Sleep.cpp 程序以使用 extern "C",如下所示:

#include <thread>
#include <chrono>

extern "C" void Sleep(int TimeMS);
extern "C"
{
   void Sleep(int TimeMs) {
    std::this_thread::sleep_for(std::chrono::milliseconds(TimeMs));
   }
}

这可以防止编译器“名称修改”函数。这样做将 Sleep() 的编译函数名称从“__Z5Sleepi”更改为“_Sleep”并缓解了我的链接器错误。

然后我将编译器调用更改为与g++ 链接而不是gcc,以便为std::__1::this_thread::sleep_for 等函数链接C++ 标准库以及C 标准库。

nasm -f macho64 Program.asm && g++ Program.o Sleep.o -o Program && ./Program

在此之后,编译器告诉我我需要将extern Sleep 更改为extern _Sleep,并且与call _Sleep 而不是call Sleep 大致相同,因为OS X 用前导_ 装饰C 符号名称。

在我完成所有这些之后,程序正确链接但产生了分段错误。 Jester 指出原因是 x86-64 调用约定不会在堆栈上传递整数/指针函数参数。您使用寄存器的方式与调用 _printf 或 _puts 的方式相同,因为这些库函数也遵循相同的标准调用约定。

在 x86-64 System V 调用约定中(用于 OS X、Linux 和 Windows 以外的所有设备),rdi is parameter 1

所以我把push 1000改成了mov rdi, 1000

完成所有这些更改后,程序可以正确编译并执行应有的操作:打印 Hello!,等待 1 秒,然后再次打印。

【讨论】:

  • 你需要使用封装函数的修饰名。不知道如何在 gcc 中获取它,在 msvc 中它由 __FUNCDNAME__ 宏返回。例如,可以在目标文件中查看。在 msvc 中可能在编译时打印 __FUNCDNAME__ 宏 - 获取准确的名称并在 asm 代码中使用它
  • 如果extern "C" 适合您,则您不需要需要使用错误名称。但是如果你需要调用一个成员函数,或者不想要一个 C++ 函数的额外包装器,你不需要是 extern "C",那么是的,你会想从 asm.xml 中调用损坏的名称。可能最好从nm 输出或其他内容中复制/粘贴它,但可能有一种方法可以让 gcc 为您制作。
  • 但可能有一种方法可以让 gcc 为您服务 - 在 msvc 中,我们可以将 __pragma(message(__FUNCDNAME__ )) 放在函数体中,然后打印正是函数名,需要在 asm.xml 中使用。当 c++ 代码中存在多个同名函数(参数不同)时,也可能出现这种情况。这不能与extern "C" 一起使用。 extern "C" 当然是最简单的解决方案,但并不总是最好的
  • 我使用extern "C"的原因仅仅是因为我正在用汇编程序编写这个程序,并且输入像“__Z5Sleepi”这样的名称非常繁琐且容易出错。 (我知道我个人几乎每次都会打错。)我并不是说没有其他方法可以做这些类型的事情,这正是我使用的方式。