pstatix
pstatix

Reputation: 3848

Why does sys.exit(4294967295) set %ERRORLEVEL% to -1?

Stumbled upon this when messing around with a batch script that invokes a Python script.

In the Python script, I was calling sys.exit(-1) which sets %ERRORLEVEL% to 4294967295. After reading that Windows uses 32-bit unsigned integers, I changed it to sys.exit(4294967295), but now the %ERRORLEVEL% is -1.

Why does it do the inverse? sys.exit(-1) makes sense because its (2^32) -1 and stored with the wrap around, but why does using the maximum value of a 32-bit unsigned integer get converted to -1? Is it something in the C underneath?

Upvotes: 0

Views: 2579

Answers (1)

Eryk Sun
Eryk Sun

Reputation: 34260

Negative integer values can be represented using a radix complement. Thus the same underlying four-byte value 0xFFFFFFFF may represent an unsigned 32-bit value 4294967295 or a signed 32-bit two's complement value -1. It's just a matter of how the bytes are interpreted in context, which includes the use of signed vs unsigned instructions such as, for example in the x86 ISA, JG (jump if greater -- signed) versus JA (jump if above -- unsigned).

When CMD waits for a process to exit, it gets the exit status via GetExitCodeProcess and saves it as the 32-bit signed integer cmd!LastRetCode. (module_name!symbol_name is how a debugger such as windbg or cdb references a global symbol name in a loaded module.) CMD interprets the exit status as a signed integer even though, at the API level, Windows returns a DWORD, which is a C typedef for unsigned long.

Normally the process exit status in Windows is an unsigned 16-bit value in the range 0-65535. Often it's pass-fail, i.e. either EXIT_SUCCESS (0) or EXIT_FAILURE (1). However, if a Windows program terminates abnormally, such as an unhandled exception, the status code will typically be an NTSTATUS value, which is a 32-bit signed integer, which represents warnings and failures as negative values. For example, an access violation is STATUS_ACCESS_VIOLATION (0xC0000005 or -1073741819).

A batch script might require special handling for when a program terminates abnormally. The last exit status can be tested with the errorlevel <number> expression, which is true if the last exit status is equal to or greater than the specified number. For example:

C:\>cmd /c exit -1073741819
C:\>if errorlevel 0 (echo normal exit) else (echo abnormal exit)
abnormal exit

CMD itself has special handling to print "^C" for STATUS_CONTROL_C_EXIT (0xC000013A or -1073741510), the exit status that indicates an unhandled console control event, which includes unhandled Ctrl+C (cancel) and Ctrl+Break. For example:

C:\>cmd /c exit -1073741510
^C

In addition to the errorlevel check, CMD has a builtin %ERRORLEVEL% environment variable. Builtin environment variables aren't actually stored in the process environment block. They're supplied as default values. In a debugger you can observe that CMD calls cmd!GetEnvVar to get the value of an environment variable. This function first tries WinAPI GetEnvironmentVariableW, which either returns a real process environment variable (e.g. PATH) or one of the OS builtin variables (e.g. __APPDIR__, __CD__). If GetEnvironmentVariableW fails to find "ERRORLEVEL", GetEnvVar defaults to the builtin value, which is the LastRetCode value converted to a string via StringCchPrintfW, with the format string "%d" (i.e. signed decimal integer). For example:

C:\>cmd /c exit -1
C:\>echo %errorlevel%
-1

C:\>set errorlevel=foo
C:\>echo %errorlevel%
foo

CMD also sets the last exit status as %=ExitCode% (conventionally hidden because the name starts with "="), which is the exit status as an unsigned 32-bit value, formatted as a zero-padded hexadecimal number. If the exit status is a printable ASCII ordinal in the range 32-126, CMD also sets %=ExitCodeAscii%. For whatever reason, CMD stores these as real environment variables that get inherited by a child process. For example, for an exit status of 65 (i.e. 0x41, i.e. the ASCII ordinal of "A"):

C:\>cmd /c exit 65

C:\>python -q
>>> import win32api
>>> win32api.GetEnvironmentVariable('=ExitCode')
'00000041'
>>> win32api.GetEnvironmentVariable('=ExitCodeAscii')
'A'

Regarding sys.exit(4294967295) in Python, note that sys.exit(status) -- and raise SystemExit(status) beneath that -- handles the exit status as a signed Python integer that gets converted to a C long int in the range -2147483648 to 2147483647. Using a value outside of this range causes the conversion to fail and sets the exit status to -1. See _Py_HandleSystemExit in the source code published on GitHub. It happens that 4294967295 (0xFFFFFFFF) is the two's complement representation of -1, but you'll get the same result for any other unsupported value. For example:

C:\>python -c "raise SystemExit(9876543210)"
C:\>echo %errorlevel%
-1

The normal exit status is 0 for success and 1 for failure, including when an error message is printed to stderr:

C:\>python -c "raise SystemExit"
C:\>echo %errorlevel%
0

C:\>python -c "raise SystemExit('whoops-a-daisy...')"
whoops-a-daisy...
C:\>echo %errorlevel%
1

Upvotes: 3

Related Questions