ExitCodes bigger than 255, possible?

Using wait() or waitpid()

It is not possible on Unix and derivatives using POSIX functions like wait() and waitpid(). The exit status information returned consists of two 8-bit fields, one containing the exit status, and the other containing information about the cause of death (0 implying orderly exit under program control, other values indicating that a signal killed it, and indicating whether a core was dumped).

Using sigaction() with SA_SIGINFO

If you work hard, and read the POSIX specification of sigaction() and <signal.h> and Signal Actions, you will find that you can get hold of the 32-bit value passed to exit() by a child process. However, it is not completely straight-forward.

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static siginfo_t sig_info = { 0 };
static volatile sig_atomic_t sig_num = 0;
static void *sig_ctxt = 0;

static void catcher(int signum, siginfo_t *info, void *vp)
{
    sig_num = signum;
    sig_info = *info;
    sig_ctxt = vp;
}

static void set_handler(int signum)
{
    struct sigaction sa;
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = catcher;
    sigemptyset(&sa.sa_mask);

    if (sigaction(signum, &sa, 0) != 0)
    {
        int errnum = errno;
        fprintf(stderr, "Failed to set signal handler (%d: %s)\n", errnum, strerror(errnum));
        exit(1);
    }
}

static void prt_interrupt(FILE *fp)
{
    if (sig_num != 0)
    {
        fprintf(fp, "Signal %d from PID %d (status 0x%.8X; UID %d)\n",
                sig_info.si_signo, (int)sig_info.si_pid, sig_info.si_status,
                (int)sig_info.si_uid);
        sig_num = 0;
    }
}

static void five_kids(void)
{
    const int base = 0xCC00FF40;
    for (int i = 0; i < 5; i++)
    {
        pid_t pid = fork();
        if (pid < 0)
            break;
        else if (pid == 0)
        {
            printf("PID %d - exiting with status %d (0x%.8X)\n",
                   (int)getpid(), base + i, base + i);
            exit(base + i);
        }
        else
        {
            int status = 0;
            pid_t corpse = wait(&status);
            if (corpse != -1)
                printf("Child: %d; Corpse: %d; Status = 0x%.4X - waited\n", pid, corpse, (status & 0xFFFF));
            struct timespec nap = { .tv_sec = 0, .tv_nsec = 1000000 }; // 1 millisecond
            nanosleep(&nap, 0);
            prt_interrupt(stdout);
            fflush(0);
        }
    }
}

int main(void)
{
    set_handler(SIGCHLD);
    five_kids();
}

When run (program sigexit73 compiled from sigexit73.c), this produces output like:

$ sigexit73
PID 26599 - exiting with status -872349888 (0xCC00FF40)
Signal 20 from PID 26599 (status 0xCC00FF40; UID 501)
Child: 26600; Corpse: 26599; Status = 0x4000 - waited
PID 26600 - exiting with status -872349887 (0xCC00FF41)
Signal 20 from PID 26600 (status 0xCC00FF41; UID 501)
Child: 26601; Corpse: 26600; Status = 0x4100 - waited
PID 26601 - exiting with status -872349886 (0xCC00FF42)
Signal 20 from PID 26601 (status 0xCC00FF42; UID 501)
Child: 26602; Corpse: 26601; Status = 0x4200 - waited
PID 26602 - exiting with status -872349885 (0xCC00FF43)
Signal 20 from PID 26602 (status 0xCC00FF43; UID 501)
Child: 26603; Corpse: 26602; Status = 0x4300 - waited
PID 26603 - exiting with status -872349884 (0xCC00FF44)
Signal 20 from PID 26603 (status 0xCC00FF44; UID 501)
$

With the one millisecond call to nanosleep() removed, the output is apt to look like:

$ sigexit73
sigexit23
PID 26621 - exiting with status -872349888 (0xCC00FF40)
Signal 20 from PID 26621 (status 0xCC00FF40; UID 501)
Child: 26622; Corpse: 26621; Status = 0x4000 - waited
PID 26622 - exiting with status -872349887 (0xCC00FF41)
PID 26623 - exiting with status -872349886 (0xCC00FF42)
Signal 20 from PID 26622 (status 0xCC00FF41; UID 501)
Child: 26624; Corpse: 26623; Status = 0x4200 - waited
Signal 20 from PID 26623 (status 0xCC00FF42; UID 501)
Child: 26625; Corpse: 26622; Status = 0x4100 - waited
PID 26624 - exiting with status -872349885 (0xCC00FF43)
PID 26625 - exiting with status -872349884 (0xCC00FF44)
$

Note that there are only three lines starting Signal here, and also only three lines ending waited; some of the signals and exit statuses are lost. This is likely to be because of timing issues between the SIGCHLD signals being set to the parent process.

However, the key point is that 4 bytes of data can be transmitted in the exit() status when the code uses sigaction(), SIGCHLD, SA_SIGINFO to track the status.

Just for the record, the testing was performed on a MacBook Pro running macOS Mojave 10.14.6, using GCC 9.2.0 and XCode 11.3.1. The code is also available in my SOQ (Stack Overflow Questions) repository on GitHub as file sigexit73.c in the src/so-1843-7779 sub-directory.


On modern Windows, the OS itself, and the default console shell (CMD.EXE), accept and show exit codes throughout at least the whole range of 32-bit signed integers. Running your example above in CMD.EXE gives the exit codes you asked for :

> java ExitCode 2
> echo %errorlevel%
2

> java ExitCode 128
> echo %errorlevel%
128

> java ExitCode 255
> echo %errorlevel%
255

> java ExitCode 256
> echo %errorlevel%
256

> java ExitCode 65536
> echo %errorlevel%
65536

Windows doesn't really have the concept of Unix signals, nor does it try to hijack the exit code to add extra information, so as long as your shell (or whatever program ends up reading the exit code) doesn't do that either, you should get back the exit codes you returned. Fortunately, programs that use Microsoft's C runtime (including all programs compiled with MS Visual C++) preserve the exit code as is from exiting processes.


Windows has many more exit codes, over 14,000. (I'm sure you often saw some of them on your own screen).

Here comes:

  • A list of Windows exit codes.
  • PowerShell script to test them.