Skip to content
Advertisement

Calling Assembly code from C++

I’m trying to write my own I/O library, so the first step is just being able to output a character. So far, this only needs to work on Linux. I wrote what I believe to be a solid _putc: label in x86 (with Intel syntax), and I put it in “io.h”, then called it from my main file, “test.cpp”. Here’s the contents of test.cpp:

#include "io.h"
extern void _putc(char) __asm__("_putc");
int main(int argc, char** argv){
    _putc('c');
    return 0;
}

and here’s my io.h:

asm(
"n.intel_syntax noprefix"
"n.globl _putc"
"n_putc:"
"ntpush [ebp]"
"ntmov ebp,esp"
"ntmov eax,4"
"ntmov ebx,0"
"ntmov ecx,[ebp+8]"
"ntmov edx,1"
"ntint 0x80"
"ntret"
"n.att_syntax" ;return to g++'s at&t syntax
);

I’m developing this on a an x86_64 Linux operating system using G++

I’m using this to compile:

 g++ -o test test.cpp -Wall

When run it segfaults immediately. I think my problem is calling convention, but there’s no way to be absolutely sure. I tried wrapping the assembly in a function (void _putc(char c)) and removing the global label, but that didn’t help. I also tried pushing the contents of ebp instead of the stuff it points to, but then it wouldn’t compile.

NOTE: This has to work with only the line #include io.h in the main file (I can move the extern void _putc(char) __asm__("_putc"); after this works), and it has to work with C++, not just C. Also, please don’t lecture me about function definitions in a header file, that’s just what I’m doing.

Advertisement

Answer

With 64-bit code you really need to consider using syscall instead of int 0x80. The system call uses a 64-bit convention and the system call numbers differ from int 0x80 in 32-bit code. You can get an idea of the 64-bit system calls in this table. The sys_writeand sys_read calls look like:

%rax  System call %rdi              %rsi             %rdx         %r10      %r8   %r9
0     sys_read    unsigned int fd   char *buf        size_t count         
1     sys_write   unsigned int fd   const char *buf  size_t count

You need to get acquainted with the System V 64-Linux ABI if you want to understand how parameters are passed in 64-bit Linux code. Among other things after a call is entered, you generally have to align the stack to a 16-byte boundary if you intend to call other functions or syscalls.

Your code also suffers from a flaw where you pass a character to sys_write. sys_write requires an address to a buffer that contains the character, not the character itself.

This is a code dump that would work:

asm(
"n.intel_syntax noprefix"
"n.globl _putc"
"n_putc:"
"ntadd rsp, -8"   // Allocate space on the stack to store the character passed
                    // Also aligns stack back to 16-bye boundary 
                    // with return address already on stack
"ntmov [rsp],rdi" // Move the character passed in RDI to memory(stack) for sys_write
"ntmov eax,1"     // With 64-bit syscalls RAX = 1 for sys_write
"ntxor edi,edi"   // 1st parameter to syscall is in RDX. FD = 0
"ntlea rsi,[rsp]" // We need to pass pointer to memory containing our char
"ntmov edx,1"     // RDX = 1 character to print. Could reuse EAX instead of 1 for src
"ntsyscall"       // Make syscall (using syscall instead of int 0x80 for 64-bit)

"ntadd rsp, 8"    // Restore stack to state right after function entry
"ntret"
"n.att_syntax prefix" //return to g++'s at&t syntax
);
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement