SIGSYS (31) 系统调用错误详解:C++ 开发者的崩溃调试指南

一、信号基础认知(开篇 5 分钟入门)

信号核心信息

  • 信号编号:31
  • 信号名称:SIGSYS (Bad System Call)
  • POSIX 标准:是(POSIX.1-2001 定义)
  • 可捕获:是
  • 默认行为:终止进程并生成 coredump

核心定位

SIGSYS 的本质作用是系统调用错误告警。当进程执行了无效的系统调用(如系统调用号不存在、参数无效、或系统调用被 seccomp 等安全机制阻止)时,操作系统会发送 SIGSYS 信号。

默认行为

Linux 内核的默认处理逻辑:

  • 终止进程:立即终止当前进程
  • 生成 coredump:如果系统配置允许,会生成 core 文件
  • 可捕获:可以捕获,但通常应该让程序终止

与 C++ 的关联性

SIGSYS 在 C++ 开发中的高发场景:

  1. 无效系统调用:使用了不存在的系统调用号
  2. seccomp 限制:程序被 seccomp 沙箱限制,禁止某些系统调用
  3. 架构不匹配:在不同架构间移植代码时,系统调用号可能不同
  4. 安全策略:容器或沙箱环境中的安全策略限制
  5. 调试工具:某些调试或分析工具可能触发

二、信号触发场景(结合 C++ 代码实例)

核心触发原因

1. 编程失误类

场景 1.1:使用无效的系统调用号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误代码:直接调用不存在的系统调用
#include <iostream>
#include <unistd.h>
#include <sys/syscall.h>

int main() {
std::cout << "Attempting invalid system call..." << std::endl;

// 使用一个不存在的系统调用号(例如 99999)
long result = syscall(99999, 0, 0, 0, 0, 0, 0);

std::cout << "Result: " << result << std::endl;
return 0;
}

Coredump 信息示例

1
2
3
4
5
6
7
8
Program received signal SIGSYS, Bad system call.
0x0000000000401123 in main () at main.cpp:8
8 long result = syscall(99999, 0, 0, 0, 0, 0, 0);
(gdb) bt
#0 0x0000000000401123 in main () at main.cpp:8
#1 0x00007ffff7e3d0b7 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) info registers
rax 0xffffffffffffffda -38 # 系统调用返回错误码
场景 1.2:系统调用参数无效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 错误代码:系统调用参数不符合要求
#include <iostream>
#include <sys/mman.h>

int main() {
// 使用无效的参数调用 mmap
// PROT_READ | PROT_WRITE | PROT_EXEC 在某些系统上可能被禁止
void* addr = mmap(nullptr, 4096,
PROT_READ | PROT_WRITE | PROT_EXEC, // 可能触发 SIGSYS(如果被 seccomp 禁止)
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);

if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}

munmap(addr, 4096);
return 0;
}

2. 系统限制类

场景 2.1:seccomp 沙箱限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 错误场景:程序在 seccomp 沙箱中运行,某些系统调用被禁止
// 这通常由外部工具(如 Docker、systemd)设置

// 示例:在 seccomp 限制的环境中
#include <iostream>
#include <sys/socket.h>

int main() {
// 如果 seccomp 禁止了 socket 系统调用,这会触发 SIGSYS
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}

close(sock);
return 0;
}

seccomp 配置示例(外部配置):

1
2
3
4
5
6
7
8
9
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["read", "write", "exit"],
"action": "SCMP_ACT_ALLOW"
}
]
}
场景 2.2:容器安全策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误场景:在容器中运行,某些系统调用被禁止
// 这通常由容器运行时(如 Docker、containerd)配置

// 程序代码本身可能正常,但在受限环境中运行会触发 SIGSYS
#include <iostream>
#include <sys/ptrace.h>

int main() {
// ptrace 系统调用在容器中通常被禁止
long result = ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
if (result < 0) {
perror("ptrace");
// 在某些配置下,这可能触发 SIGSYS 而非返回错误
}

return 0;
}

3. 运行时异常类

场景 3.1:架构不匹配的系统调用
1
2
3
4
5
6
7
8
9
// 错误场景:在不同架构间移植代码
// 系统调用号在不同架构上可能不同

// x86_64 上的系统调用号
#define SYS_read 0
#define SYS_write 1

// ARM 上的系统调用号可能不同
// 如果代码硬编码了系统调用号,在不同架构上可能触发 SIGSYS

易混淆场景辨析

SIGSYS vs SIGILL

SIGSYS:系统调用错误(系统调用号无效、参数错误、被禁止)

1
syscall(99999);  // SIGSYS:无效的系统调用号

SIGILL:非法指令(CPU 无法执行的指令)

1
__asm__ volatile (".byte 0x0f, 0x0b\n");  // SIGILL:非法指令

SIGSYS vs EINVAL

  • SIGSYS:系统调用本身无效或被禁止(由内核触发)
  • EINVAL:系统调用有效,但参数无效(返回错误码,不触发信号)

三、崩溃调试与定位(实操步骤)

基础定位:core 文件 + gdb 调试

步骤 1:开启 core 文件

1
ulimit -c unlimited

步骤 2:gdb 加载 core 文件

1
2
3
g++ -g -O0 -o program main.cpp
./program # 会崩溃并生成 core
gdb ./program core

步骤 3:关键调试命令

1
2
3
4
5
(gdb) bt                    # 查看调用栈
(gdb) info registers # 查看寄存器(rax 包含系统调用号和返回值)
(gdb) print $rax # 查看系统调用返回值
(gdb) x/10i $pc-20 # 查看系统调用指令
(gdb) disas # 反汇编当前函数

实际调试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ gdb ./sigsys_example core
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
...
Core was generated by `./sigsys_example'.
Program terminated with signal SIGSYS, Bad system call.
#0 0x0000000000401123 in main () at main.cpp:8
8 long result = syscall(99999, 0, 0, 0, 0, 0, 0);

(gdb) bt
#0 0x0000000000401123 in main () at main.cpp:8
#1 0x00007ffff7e3d0b7 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6

(gdb) info registers
rax 0xffffffffffffffda -38 # -ENOSYS (Function not implemented)
rbx 0x0 0
rcx 0x1869f 99999 # 系统调用号
rdx 0x0 0
rsi 0x0 0
rdi 0x0 0

(gdb) x/5i $pc-10
0x401118 <main+8>: mov $0x1869f,%eax # 系统调用号 99999
0x40111d <main+13>: syscall # 系统调用指令
0x40111f <main+15>: mov %rax,-0x8(%rbp) # 保存返回值

进阶工具

工具 1:strace 跟踪系统调用

1
2
3
4
5
# 跟踪系统调用,查看哪些系统调用失败
strace -e trace=all ./program 2>&1 | grep -i "syscall\|sigsys"

# 跟踪特定系统调用
strace -e trace=mmap,socket,ptrace ./program

strace 输出示例

1
2
3
syscall_99999(0, 0, 0, 0, 0, 0) = -1 ENOSYS (Function not implemented)
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x40111d, si_syscall=99999, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS +++

工具 2:检查 seccomp 配置

1
2
3
4
5
# 检查进程的 seccomp 状态
cat /proc/<PID>/status | grep Seccomp

# 检查容器的 seccomp 配置
docker inspect <container> | grep -i seccomp

工具 3:使用 libseccomp 检查

1
2
# 如果程序使用 libseccomp,检查配置
# 这需要程序支持或使用外部工具

定位关键点

SIGSYS 崩溃的核心排查方向:

  1. 检查系统调用号:确认使用的系统调用号是否有效
  2. 检查 seccomp 配置:确认是否有 seccomp 限制
  3. 检查容器环境:确认是否在受限容器中运行
  4. 检查架构匹配:确认系统调用号是否与架构匹配
  5. 检查系统调用参数:确认参数是否有效

四、崩溃修复方案(针对性解决)

分场景修复代码

场景 1:无效系统调用号

快速修复:使用正确的系统调用
1
2
3
4
5
6
7
8
// 修复前
long result = syscall(99999, 0, 0, 0, 0, 0, 0); // 无效的系统调用号

// 快速修复:使用标准库函数而非直接系统调用
#include <unistd.h>

// 使用标准库函数(内部会调用正确的系统调用)
pid_t pid = getpid(); // 而非 syscall(SYS_getpid)
优雅修复:使用标准库或检查系统调用可用性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 优雅修复方案 1:使用标准库函数
#include <unistd.h>
#include <sys/syscall.h>

// 使用标准库函数,让库处理系统调用细节
pid_t pid = getpid();

// 优雅修复方案 2:检查系统调用是否可用(如果必须使用 syscall)
#include <errno.h>

long result = syscall(SYS_getpid);
if (result < 0 && errno == ENOSYS) {
// 系统调用不可用,使用替代方案
pid_t pid = getpid();
}

场景 2:seccomp 限制

快速修复:避免被禁止的系统调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 修复前:使用可能被 seccomp 禁止的系统调用
#include <sys/ptrace.h>

long result = ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);

// 快速修复:检查错误并处理
#include <errno.h>

long result = ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
if (result < 0) {
if (errno == EPERM || errno == EINVAL) {
// 系统调用被禁止或不可用
std::cerr << "ptrace not available" << std::endl;
// 使用替代方案
}
}
优雅修复:使用功能检测和降级方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 优雅修复:检测功能可用性并提供降级方案
#include <sys/ptrace.h>
#include <errno.h>

class Tracer {
private:
bool ptraceAvailable;

public:
Tracer() {
// 检测 ptrace 是否可用
ptraceAvailable = checkPtraceAvailable();
}

bool trace() {
if (!ptraceAvailable) {
// 使用替代方案
return alternativeTrace();
}

long result = ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
return result == 0;
}

private:
bool checkPtraceAvailable() {
// 尝试调用 ptrace 检测是否可用
errno = 0;
long result = ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
if (result < 0 && errno == EPERM) {
return false; // 被禁止
}
return true;
}

bool alternativeTrace() {
// 替代方案
return true;
}
};

场景 3:架构不匹配

快速修复:使用条件编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修复前:硬编码系统调用号
long result = syscall(0); // 假设是 read,但在不同架构上可能不同

// 快速修复:使用条件编译
#include <sys/syscall.h>

#ifdef __x86_64__
#define SYS_READ 0
#elif defined(__aarch64__)
#define SYS_READ 63
#elif defined(__arm__)
#define SYS_READ 3
#endif

long result = syscall(SYS_READ, fd, buf, count);
优雅修复:使用标准库或自动检测
1
2
3
4
// 优雅修复:使用标准库函数(自动处理架构差异)
#include <unistd.h>

ssize_t result = read(fd, buf, count); // 标准库处理架构差异

修复验证

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <gtest/gtest.h>

TEST(SyscallTest, ValidSyscall) {
pid_t pid = getpid();
EXPECT_GT(pid, 0);
}

TEST(SyscallTest, InvalidSyscallHandling) {
// 测试无效系统调用的处理
errno = 0;
long result = syscall(99999);
EXPECT_LT(result, 0);
EXPECT_EQ(errno, ENOSYS);
}

避坑提醒

  1. 避免直接使用 syscall:优先使用标准库函数,让库处理系统调用细节
  2. 检查 seccomp 环境:在受限环境中运行时,避免使用被禁止的系统调用
  3. 处理架构差异:不要硬编码系统调用号,使用标准库或条件编译
  4. 错误处理:检查系统调用返回值,处理 ENOSYS 等错误

五、长期预防策略(从编码到部署全链路)

编码规范

C++ 开发中规避 SIGSYS 的编码习惯:

  1. 使用标准库函数:避免直接调用 syscall

    1
    read(fd, buf, size);  // 而非 syscall(SYS_read, ...)
  2. 检查系统调用返回值

    1
    2
    3
    4
    5
    6
    long result = syscall(...);
    if (result < 0) {
    if (errno == ENOSYS) {
    // 系统调用不可用
    }
    }
  3. 处理受限环境:检测并适应 seccomp 等限制

    1
    2
    3
    if (isSeccompRestricted()) {
    // 使用受限的功能集
    }

编译阶段

开启防御性编译选项:

1
2
3
4
# 启用警告
g++ -g -O2 -Wall -Wextra \
-Werror=implicit-function-declaration \
-o program main.cpp

测试策略

1
2
3
4
5
6
7
8
9
10
11
12
// 测试系统调用错误处理
TEST(SyscallTest, ErrorHandling) {
errno = 0;
long result = syscall(99999);
EXPECT_LT(result, 0);
EXPECT_EQ(errno, ENOSYS);
}

// 测试受限环境
TEST(RestrictedTest, SeccompEnvironment) {
// 测试在受限环境中的行为
}

线上监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <signal.h>
#include <execinfo.h>
#include <iostream>
#include <sys/syscall.h>

void sysHandler(int sig, siginfo_t* info, void* context) {
std::cerr << "SIGSYS caught!" << std::endl;
std::cerr << "System call number: " << info->si_syscall << std::endl;
std::cerr << "Architecture: " << info->si_arch << std::endl;

void* array[10];
size_t size = backtrace(array, 10);
backtrace_symbols_fd(array, size, STDERR_FILENO);

// 记录日志
// logToFile("SIGSYS occurred", info);

exit(1);
}

int main() {
struct sigaction sa;
sa.sa_sigaction = sysHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;

sigaction(SIGSYS, &sa, nullptr);

// 程序代码
return 0;
}

六、拓展延伸(加深理解)

相关信号对比

SIGSYS vs SIGILL vs SIGSEGV

特性 SIGSYS SIGILL SIGSEGV
触发原因 系统调用错误 非法指令 内存访问违规
常见场景 无效系统调用、seccomp 限制 非法指令 空指针、越界
可捕获

进阶技巧:检测 seccomp 限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/syscall.h>
#include <errno.h>
#include <iostream>

bool isSeccompRestricted() {
// 尝试一个可能被禁止的系统调用
errno = 0;
long result = syscall(SYS_ptrace, PTRACE_TRACEME, 0, nullptr, nullptr);

if (result < 0) {
if (errno == EPERM) {
// 可能被 seccomp 禁止
return true;
}
}

return false;
}

int main() {
if (isSeccompRestricted()) {
std::cout << "Running in restricted environment" << std::endl;
// 使用受限的功能集
}

return 0;
}

实际案例分享

案例:容器中系统调用被禁止

问题描述:程序在 Docker 容器中运行崩溃,coredump 显示 SIGSYS。

排查过程

  1. GDB 分析显示崩溃在系统调用
  2. 检查 seccomp 配置,发现某些系统调用被禁止
  3. 使用 strace 确认被禁止的系统调用

根本原因

1
2
3
4
5
6
7
// 问题代码:使用可能被容器禁止的系统调用
#include <sys/ptrace.h>

int main() {
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr); // 在容器中被禁止
return 0;
}

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 修复代码:检测并处理
#include <sys/ptrace.h>
#include <errno.h>

int main() {
errno = 0;
long result = ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
if (result < 0) {
if (errno == EPERM) {
// 被禁止,使用替代方案
std::cerr << "ptrace not available in this environment" << std::endl;
}
}

return 0;
}

总结

SIGSYS 是系统调用错误信号,主要原因是无效的系统调用或被安全机制禁止。通过:

  1. 使用标准库:避免直接调用 syscall,使用标准库函数
  2. 检查返回值:处理 ENOSYS 等错误
  3. 适应受限环境:检测并适应 seccomp 等限制
  4. 调试技巧:使用 strace 跟踪系统调用,GDB 分析 coredump
  5. 预防策略:编码规范、测试覆盖、环境检测

可以有效减少 SIGSYS 崩溃,提高程序在不同环境中的兼容性。