0

I'm currently learning assembly and I would need some help with this simple program.

The say_something() function is created by x86_64 assembly (using NASM on 64-bit Linux) and is simply supposed to print out the phrase and the newline.

The main program is written in C and looks like this:

extern void say_something(char *);

int main(void) {
    say_something("First line.");
    say_something("Second sentence on another line.");

    return 0;
}

... and the content of the *.asm file is this:

section .text
    global say_something

say_something:
    push rbp
    mov rbp, rsp

    mov rax, 4 ; syscall for write
    mov rbx, 1
    mov rcx, rdi
    int 0x80

    pop rbp
    ret

But apparently my ASM function is buggy, because when I compile the program and run it, the output is just some junk, whereas it's supposed to print this:

First line.
Second sentence on another line.

What changes should I do on my assembly part to make it work correctly?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 1
    Isn't `sys_write` 1 for x86_64? Also, you should set `rdx` to the number of bytes to write. – Michael Apr 09 '18 at 08:50
  • See http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ – Michael Apr 09 '18 at 08:52
  • 1
    amd64 system calls are done with syscall, not int $0x80 and have different numbers and a different calling convention. Who teaches beginners to do it wrongly like OP does? – fuz Apr 09 '18 at 09:36
  • 2
    Possible duplicate of [Why cant i sys\_write from a register?](https://stackoverflow.com/questions/10105871/why-cant-i-sys-write-from-a-register) – fuz Apr 09 '18 at 10:59
  • @Michael: This code is (unwisely) [using the 32-bit `int 0x80` ABI from 64-bit code, so the call numbers are from `unistd_32.h`](https://stackoverflow.com/questions/46087730/what-happens-if-you-use-the-32-bit-int-0x80-linux-abi-in-64-bit-code). The arg-passing registers are ebx,ecx,edx, etc. (*not* rdi,rsi,rdx, etc.), so you can only use 32-bit pointers and lengths. But anyway, yes, you need `edx=strlen()` – Peter Cordes Apr 09 '18 at 22:07
  • @fuz: not really a duplicate. The OP says it prints junk, rather than nothing, so it should work if they set `edx=strlen()`. – Peter Cordes Apr 09 '18 at 22:30

1 Answers1

0

You have one major problem here, and two other major problems that might not be visible with this particular caller:

  • You forgot to set edx to the string length. The system call you're using is
    sys_write(int fd, const char *buf, size_t len);, so the len you're passing is whatever garbage your caller left in edx.

If that doesn't make sys_write return -EFAULT without printing anything, then maybe you're printing a bunch of binary garbage before it detects a problem, if you ended up with a very large len? Hmm, write probably checks the whole range before writing anything, so maybe you ended up with a medium-sized value in edx and just printed some binary garbage after your strings. Your strings should be there at the start of your output, though.

Use strace ./my_program to find out, or GDB to look at edx before the call. (strace won't actually work right for int 0x80 from 64-bit code; it decodes as if you were using the native 64-bit syscall ABI).

  • You clobber the caller's rbx without saving/restoring it, violating the calling convention (the x86-64 System V ABI) where rbx is a call-preserved register. See http://agner.org/optimize/ for a nice doc about calling conventions. This may have no effect; your main probably doesn't compile to code that uses rbx for anything, so the rbx you're destroying is "owned" by the CRT start function that calls main.

    Strangely, you save/restore rbp even though you don't use it at all.

Most importantly, you're using the 32-bit int 0x80 ABI from 64-bit code!! This is why I've been saying that write takes its length are in edx, not rdx, because the 32-bit int 0x80 ABI is exactly the same ABI that 32-bit user-space can use, where it only looks at the low 32 bits of registers.

Your code will break with pointers outside the low 32 bits of virtual address space. e.g. if you compiled with gcc -fPIE -pie, even those string literals in the .rodata section will be outside the low 32 bits, and sys_write will probably return -EFAULT without doing anything.

-pie is the default for gcc on many modern Linux distros, so you're "lucky"(?) that your program printed anything at all instead of just having write fail with -EFAULT. You aren't checking error return values, and it doesn't segfault your program the way it would if you tried to read/write from a bad point in user-space, so it fails silently.

As @fuz points out, Why cant i sys_write from a register? shows some example code for using the 64-bit syscall for sys_write, but it hard-codes a length. You're going to need to strlen your string, either with the libc function or with a loop. (Or a slow but compact repe scasb).

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847