Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I link a C++ subroutine to an x86 assembly program?

I am trying to make a simple assembly program that prints "Hello!" once, waits one second, then prints it again. Since sleep functions are relatively complex in assembly, and I'm not that good at it, I decided using C++ would be the way to go to make the Sleep subroutine. Here's the C++ program:

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

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

I then compiled this sleep function into an assembly program using "gcc -S Sleep.cpp" then compiled it into an object file using "gcc -c Sleep.s"

I am trying to call this C++ subroutine from assembly. I heard that you provide parameters to C++ subroutines by pushing them onto the stack, here's my assembly code so far:

        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

Both these files are saved to Desktop/Program. I'm trying to compile it using NASM and GCC, my compiler invocation is:

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

But I get the error:

"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)

Changing the code to "extern __Z5Sleepi" and calling "__Z5Sleepi" instead of Sleep doesn't seem to fix the problem. (I get the same error message just without the "Maybe you meant __Z5Sleepi" bit. I also tried using _Sleep instead of Sleep without success.) What am I doing wrong? How do I properly use and link this C++ subroutine with my assembly program? Is the method I am using so far to do this just wrong from the ground up?

Any help is much appreciated, browsing stack overflow, there seem to be a lot of questions regarding this but none of them actually go into the linking process. (And they seem to be asking about linking assembly with C++, not C++ with assembly.) I am using NASM and GCC to compile, and my platform is Mac OSX.

like image 518
SectorSam Avatar asked Sep 24 '18 14:09

SectorSam


1 Answers

As Jester pointed out, the problem arose from two things. One was I needed to change the Sleep.cpp program to use extern "C", like this:

#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));
   }
}

This prevents the compiler from "name mangling" the function. Doing that changed the compiled function name of Sleep() from "__Z5Sleepi" to "_Sleep" and alleviated my linker errors.

Then I changed my compiler invocation to link with g++ instead of gcc, to link the C++ standard library for functions like std::__1::this_thread::sleep_for, as well as the C standard library.

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

After this, the compiler told me I needed to change extern Sleep to extern _Sleep and much the same with call _Sleep instead of call Sleep, because OS X decorates C symbol names with a leading _.

After I did all this, the program linked properly but produced a segmentation fault. Jester pointed out the reason for this is that x86-64 calling conventions don't pass integer/pointer function parameters on the stack. You use the registers in the same way you would call _printf or _puts, because those library functions also follow the same standard calling convention.

In the x86-64 System V calling convention (used on OS X, Linux, and everything other than Windows), rdi is parameter 1.

So I changed push 1000 to mov rdi, 1000

After all these changes were made, the program compiles correctly and does exactly what it should: Print Hello!, wait 1 second, then print it again.

like image 135
SectorSam Avatar answered Oct 11 '22 18:10

SectorSam