环境
-
系统: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 |