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_write
and 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 );