環境
-
系統:macOS M1
-
Shell:Zsh
-
編譯器:clang++ 13.1.6
return -1
和 255
一個簡單的 C++ 小程序:
int main() { return -1; }
編譯執行後,查看程序的退出碼爲 255:
❯ echo $?
255
爲什麼程序裏返回的是 -1,但是系統中查看的退出碼卻是 255 呢?
分析
我們知道如果是非零的退出碼,表示命令執行失敗,嘗試更多的情況:
Return | Exit Code |
---|---|
-1 | 255 |
-2 | 254 |
-10 | 246 |
1 | 1 |
基於此,我們先做一些猜測:
-
退出碼可能是一個 unsigned 的類型,長度可能是 1 個字節。
-
上述情況中,-1 和 255, -2 和 254,以及 -10 和 246 之間應該存在某種規律,可能是一種強制類型轉換。
針對以上猜測,我們需要進一步瞭解負數在內存中是如何存儲的。
負數的表示
在計算機中,數值統一使用補碼來表示和存儲。其中,正數的補碼和其原碼相同,負數的補碼是將其原碼除符號位外,其餘取反後加 1。
以 -1 爲例,假設它爲 signed char 類型,那麼它的原碼爲 1000 0001,補碼爲 1111 1111,如果將其強制轉換爲 unsigned char 類型,它的十進制表示的就是 255。
其它情況的轉換結果如下:
signed char | 原碼 | 補碼 | unsigned char |
---|---|---|---|
-1 | 1000 0001 | 1111 1111 | 255 |
-2 | 1000 0010 | 1111 1110 | 254 |
-10 | 1000 1010 | 1111 0110 | 246 |
1 | 0000 0001 | 0000 0001 | 1 |
我們也可以使用程序計算上述結果,以 Python 爲例:
>>> import array
>>> arr = array.array("b", [-1, -2, -10, 1])
>>> arr_v = memoryview(arr)
>>> arr_v.cast("B").tolist()
[255, 254, 246, 1]
fork 和 exec
簡單來說,當我們在 Shell 中執行上述編譯後的二進制可執行文件時,Shell 會先 fork 出一個子進程,然後在子進程中執行命令,命令的退出狀態會再被父進程(Shell)收集。
不包含 Shell 的內建命令,如
cd
、alias
等,這些內建命令不會創建子進程。
收集命令結果
Shell 本質上也是一個程序,可以通過 C 語言庫函數中的 wait()
方法收集子進程返回的結果。
pid_t wait(int *status)
其中,status
的主要字段結構如下:
15 8 7 6 0
+-----------------------------------+
| 退出碼 | core dump 標識位 | 信號 |
+-----------------------------------+
一般而言,程序退出的方式有兩種:
-
程序正常結束,例如:
return
或者exit
等; -
程序異常終止,例如:CTRL + C 或者 kill 等;
我們可以通過一些程序對上述兩種情況做一些測試。
- 程序正常結束:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t c_pid;
int status;
c_pid = fork();
if (c_pid == 0) {
exit(-10); /* 子進程退出 */
} else {
wait(&status); /* 收集子進程的狀態 */
}
printf("Child exit code: %d\n", WEXITSTATUS(status));
return 0;
}
編譯執行上述程序:
Child exit code: 246
這裏我們使用了宏函數 WEXITSTATUS
來獲取退出碼,在我的系統中,它的實際含義爲:
#define _W_INT(w) (*(int *)&(w))
#define WEXITSTATUS(x) ((_W_INT(x) >> 8) & 0x000000ff)
- 程序異常終止:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t c_pid;
int status;
c_pid = fork();
if (c_pid == 0) {
for (;;) /*死循環*/
;
} else {
kill(c_pid, SIGKILL);
wait(&status);
}
printf("Child exit signal: %d\n", WTERMSIG(status));
return 0;
}
編譯執行上述程序:
Child exit signal: 9
這裏我們向子進程發送 SIGKILL 信號,殺死進程,和 kill -9 WTERMSIG
宏函數解析出導致進程終止的信號值。
俗成的約定
類似 HTTP 服務中的 404、501 這樣的狀態碼,針對 Shell 中命令的退出碼也有一些默認俗成的約定:
Exit Code Number | Meaning | Example | Comments |
---|---|---|---|
1 | Catchall for general errors | var1 = 1/0 |
Miscellaneous errors, such as "divide by zero" and other impermissible operations |
126 | Command invoked cannot execute | /dev/null | Permission problem or command is not an executable |
127 | "command not found" | illegal_command | Possible problem with $PATH or a typo |
128 | Invalid argument to exit | exit 3.14159 | exit takes only integer args in the range 0 - 255 (see first footnote) |
128+n | Fatal error signal "n" | kill -9 $PPID of script | $? returns 137 (128 + 9) |
130 | Script terminated by Control-C | Ctl-C | Control-C is fatal error signal 2, (130 = 128 + 2, see above) |
255* | Exit status out of range | exit -1 | exit takes only integer args in the range 0 - 255 |