Reputation: 3848
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
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