3

I decided yesterday to learn assembly (NASM syntax) after years of C++ and Python and I'm already confused about the way to exit a program. It's mostly about ret because it's the suggested instruction on SASM IDE.

I'm speaking for main obviously. I don't care about x86 backward compatibility. Only the x64 Linux best way. I'm curious.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Etienne Armangau
  • 255
  • 2
  • 10
  • When you return from `main()`, the standard runtime library calls the `exit()` system call, so I think they're equivalent. – Barmar May 24 '21 at 17:49
  • If you are using the C library, as is hinted by `main`, then you should not use a system call. The simplest is to `ret`. You can also `call exit` which is useful from deeper in the call stack. – Jester May 24 '21 at 18:33

1 Answers1

9

If you use printf or other libc functions, it's best to ret from main or call exit. (Which are equivalent; main's caller will call the libc exit function.)

If not, if you were only making other raw system calls like write with syscall, it's also appropriate and consistent to exit that way, but either way, or call exit are 100% fine in main.

If you want to work without libc at all, e.g. put your code under _start: instead of main: and link with ld or gcc -static -nostdlib, then you can't use ret. Use mov eax, 231 (__NR_exit_group) / syscall.

main is a real & normal function like any other (called with a valid return address), but _start (the process entry point) isn't. On entry to _start, the stack holds argc and argv, so trying to ret would set RIP=argc, and then code-fetch would segfault on that unmapped address. Nasm segmentation fault on RET in _start


System call vs. ret-from-main

Exiting via a system call is like calling _exit() in C - skip atexit() and libc cleanup, notably not flushing any buffered stdout output (line buffered on a terminal, full-buffered otherwise). This leads to symptoms such as Using printf in assembly leads to empty output when piping, but works on the terminal (or if your output doesn't end with \n, even on a terminal.)

main is a function, called (indirectly) from CRT startup code. (Assuming you link your program normally, like you would a C program.) Your hand-written main works exactly like a compiler-generate C main function would. Its caller (__libc_start_main) really does do something like int result = main(argc, argv); exit(result);,
e.g. call rax (pointer passed by _start) / mov edi, eax / call exit.
So returning from main is exactly1 like calling exit.

  • Syscall implementation of exit() for a comparison of the relevant C functions, exit vs. _exit vs. exit_group and the underlying asm system calls.

  • C question: What is the difference between exit and return? is primarily about exit() vs. return, although there is mention of calling _exit() directly, i.e. just making a system call. It's applicable because C main compiles to an asm main just like you'd write by hand.

Footnote 1: You can invent a hypothetical intentionally weird case where it's different. e.g. you used stack space in main as your stdio buffer with sub rsp, 1024 / mov rsi, rsp / ... / call setvbuf. Then returning from main would involve putting RSP above that buffer, and __libc_start_main's call to exit could overwrite some of that buffer with return addresses and locals before execution reached the fflush cleanup. This mistake is more obvious in asm than C because you need leave or mov rsp, rbp or add rsp, 1024 or something to point RSP at your return address.

In C++, return from main runs destructors for its locals (before global/static exit stuff), exit doesn't. But that just means the compiler makes asm that does more stuff before actually running the ret, so it's all manual in asm, like in C.

The other difference is of course the asm / calling-convention details: exit status in EAX (return value) or EDI (first arg), and of course to ret you have to have RSP pointing at your return address, like it was on function entry. With call exit you don't, and you can even do a conditional tailcall of exit like jne exit. Since it's a noreturn function, you don't really need RSP pointing at a valid return address. (RSP should be aligned by 16 before a call, though, or RSP%16 = 8 before a tailcall, matching the alignment after call pushes a return address. It's unlikely that exit / fflush cleanup will do any alignment-required stores/loads to the stack, but it's a good habit to get this right.)

(This whole footnote is about ret vs. call exit, not syscall, so it's a bit of a tangent from the rest of the answer. You can also run syscall without caring where the stack-pointer points.)


SYS_exit vs. SYS_exit_group raw system calls

The raw SYS_exit system call is for exiting the current thread, like pthread_exit().
(eax=60 / syscall, or eax=1 / int 0x80).

SYS_exit_group is for exiting the whole program, like _exit.
(eax=231 / syscall, or eax=252 / int 0x80).

In a single-threaded program you can use either, but conceptually exit_group makes more sense to me if you're going to use raw system calls. glibc's _exit() wrapper function actually uses the exit_group system call (since glibc 2.3). See Syscall implementation of exit() for more details.

However, nearly all the hand-written asm you'll ever see uses SYS_exit1. It's not "wrong", and SYS_exit is perfectly acceptable for a program that didn't start more threads. Especially if you're trying to save code size with xor eax,eax / inc eax (3 bytes in 32-bit mode) or push 60 / pop rax (3 bytes in 64-bit mode), while push 231/pop rax would be even larger than mov eax,231 because it doesn't fit in a signed imm8.

Note 1: (Usually actually hard-coding the number, not using __NR_... constants from asm/unistd.h or their SYS_... names from sys/syscall.h)

And historically, it's all there was. Note that in unistd_32.h, __NR_exit has call number 1, but __NR_exit_group = 252 wasn't added until years later when the kernel gained support for tasks that share virtual address space with their parent, aka threads started by clone(2). This is when SYS_exit conceptually became "exit current thread". (But one could easily and convincingly argue that in a single-threaded program, SYS_exit does still mean exit the whole program, because it only differs from exit_group if there are multiple threads.)

To be honest, I've never used eax=252 / int 0x80 in anything, only ever eax=1. It's only in 64-bit code where I often use mov eax,231 instead of mov eax,60 because neither number is "simple" or memorable the way 1 is, so might as well be a cool guy and use the "modern" exit_group way in my single-threaded toy program / experiment / microbenchmark / SO answer. :P (If I didn't enjoy tilting at windmills, I wouldn't spend so much time on assembly, especially on SO.)

And BTW, I usually use NASM for one-off experiments so it's inconvenient to use pre-defined symbolic constants for call numbers; with GCC to preprocess a .S before running GAS you can make your code self-documenting with #include <sys/syscall.h> so you can use mov $SYS_exit_group, %eax (or $__NR_exit_group), or mov eax, __NR_exit_group with .intel_syntax noprefix.


Don't use the 32-bit int 0x80 ABI in 64-bit code:

What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? explains what happens if you use the COMPAT_IA32_EMULATION int 0x80 ABI in 64-bit code.

It's totally fine for just exiting, as long as your kernel has that support compiled in, otherwise it will segfault just like any other random int number like int 0x7f. (e.g. on WSL1, or people that built custom kernels and disabled that support.)

But the only reason you'd do it that way in asm would be so you could build the same source file with nasm -felf32 or nasm -felf64. (You can't use syscall in 32-bit code, except on some AMD CPUs which have a 32-bit version of syscall. And the 32-bit ABI uses different call numbers anyway so this wouldn't let the same source be useful for both modes.)


Related:

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Thanks Peter, it's clearer in my mind. I prefer to start on good bases. – Etienne Armangau May 24 '21 at 18:42
  • @SepRoland: Thanks, fixed. (Feel free to fix typos yourself if you want; although in this case I ended up making a couple more tweaks). – Peter Cordes May 24 '21 at 20:57
  • I'll remember, although I think there might be other people that really dislike having their text fixed... – Sep Roland May 24 '21 at 20:59
  • 1
    @SepRoland: I certainly appreciate people tidying up editing mistakes in my answers, like typos or a repeated word, or a sentence I started one way, then went back and rephrased without fully removing the old words. People that object even to typo fixes that don't change phrasing / meaning are IMO missing the point of Stack Overflow (creating a library of useful answers). IMO leaving comments instead of editing is a valid choice when the answer is actually wrong (e.g. phrasing implies something incorrect about cases other than the one being asked about). People can always roll back, though. – Peter Cordes May 24 '21 at 21:22
  • Nice answer! But why use `exit_group`? In my experience (unless done during an unrecoverable, unexpected, error) this is always a code smell. If one has multiple threads, either one joins them before calling `exit` or explicitly marks them detached (a.k.a. "daemon threads" in Java/Python) so they are automatically killed by `exit`. – Margaret Bloom May 25 '21 at 07:26
  • 1
    @MargaretBloom: The `exit_group` system call is how processes normally exit, via `exit(3)`. Even `_exit(2)` uses `SYS_exit_group` in glibc2.3 and later ([Syscall implementation of exit()](https://stackoverflow.com/q/46903180)), so I assumed SYS_exit was essentially considered obsolete, and/or that `exit_group` is the normal way to exit. I guess `pthread_exit` would use SYS_exit, unless threads have some other way of exiting. – Peter Cordes May 25 '21 at 07:31
  • 1
    @MargaretBloom: but `pthread_exit` isn't just a syscall wrapper; it does some cleanup. And if this is the last thread, then it runs exit(3) -> SYS_exit_group. But if not, then yes I guess it would run SYS_exit. So AFAIK, glibc doesn't have a wrapper function for SYS_exit, only an eventual use inside pthreads. Your argument that it's a bad idea to intentionally use exit_group to kill other threads, but accidentally leaving them running in case of bugs is even worse. Moot point in a single-threaded simple toy program, so it's more of a "remember this is what glibc does" thing. – Peter Cordes May 25 '21 at 07:43
  • @PeterCordes I didn't know `exit` used `exit_group`, I stand corrected :) – Margaret Bloom May 25 '21 at 09:40