I wonder if the reason why the first shell treated zero as a success code is because the "edi" register is automatically set to zero when the process starts. We can test this out quickly by writing a program that calls the exit syscall without explicitly setting ebi to zero:
write "test.asm" segment .text
global _start
_start:
mov eax,60
syscall
end
Then, we can build an executable from this assembly source file using Yasm and ld:
build test test.asm yasm -f elf64 -g dwarf2 test.asm && ld -o test test.o
Executing test yields zero as the process return code.
./test
echo $?
$ 0
What about if we don't explicitly exit using the exit syscall?
write "test_2.asm" segment .text
global _start
_start:
mov eax,60
end
build test_2 test_2.asm yasm -f elf64 -g dwarf2 test_2.asm && ld -o test_2 test_2.o
That resulted in a segmentation fault, which is really interesting.
strace ./test_2
execve("./test_2", ["./test_2"], 0x7ffffc98c800 /* 33 vars */) = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x3c} ---
+++ killed by SIGSEGV +++
Segmentation fault
It turns out, without explicitly calling the exit system call at the end of the program, the CPU continues to exit past the program's intructions into invalid memory. The main function in C sets up the system call to exit automatically, so we aren't normally aware of this as C programmers.
More realistically, 0 is probably the choice for success because having nonzero represent an error means that different errors can be encoded in different nonzero return codes.