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