不从main开始执行C++程序

在看蒋炎炎老师的OS课后一段时间记录下的操作,记得当时实现了另一种作法,但未及时记录,现在也忘了怎么复刻😅

如何让C/C++程序不从main开始执行

一、核心操作步骤(以 GCC/G++ 为例)

test.cpp为例,代码需手动定义_start(汇编级入口,需处理系统调用),而非main

1. 编写示例代码

_start是 Linux 内核规定的程序入口(ELF 文件格式定义),需用汇编或内嵌汇编实现系统调用(如exit),否则程序会崩溃。

1
2
3
4
5
6
7
8
9
// test.cpp
void _start() {
// 内嵌汇编:调用Linux系统调用 exit(0),避免程序异常退出
asm volatile (
"mov $60, %rax;" // 系统调用号:60对应exit
"mov $0, %rdi;" // 退出码:0(成功)
"syscall;" // 触发系统调用
);
}

2. 编译与链接参数

关键参数用于禁用自动链接的初始化代码指定入口点,分两步执行或合并为一条命令:

  • 分步执行

    1. 编译为目标文件(仅编译,不链接):g++ -c test.cpp -o test.o
    2. 链接为可执行文件(禁用 libc 和 crt0):ld -o test test.o -nostdlib -e _start
  • 合并执行(更简洁):
    g++ -o test test.cpp -nostdlib -e _start

  • g++ test.cpp -nostartfiles -nostdlib -static -o test

参数 作用说明
-nostdlib 不链接 C 标准库(libc)和运行时初始化代码(crt0、crtbegin 等),这是核心
-e _start 显式指定程序入口点为_start(默认入口由 crt0 定义,最终调用main

二、必须注意的 3 个关键点

  1. 不能使用任何 C++/C 标准库功能
    由于-nostdlib禁用了 libc,coutprintfnewmallocstring等所有依赖标准库的代码都会编译失败,甚至return语句也无效(需手动调用系统调用退出)。

  2. 必须手动处理程序退出
    内核要求程序通过系统调用(如exit)终止,若_start执行完直接返回,会触发内核异常(段错误)。示例中通过内嵌汇编调用exit(0),是唯一正确的退出方式。

  3. C++ 的 “高级特性” 失效
    没有运行时初始化代码(crt0),C++ 的全局对象构造 / 析构、虚函数表初始化、异常处理(try/catch)等功能全部无法工作,代码本质退化为 “用 C++ 语法写的汇编级程序”。

三、背后的底层原理

核心是揭开 “main 函数是入口” 的假象,理解程序从内核加载到执行main的完整链路:

  1. 内核加载程序时的入口是_start
    Linux 下可执行文件为 ELF 格式,ELF 头中会记录 “程序入口地址”。默认情况下,链接器会将入口地址设为crt0.o(C 运行时初始化文件)中的_start符号,这是内核执行的第一个函数,而非main

  2. main是被crt0调用的 “用户级入口”
    默认流程(未禁用 [1] 时):内核加载→执行crt0的_start→初始化libc(堆、全局变量、I/O等)→调用main→main返回后调用exit→程序终止
    可见,main是 crt0 为用户代码提供的 “封装入口”,而非内核直接调用的入口。

  3. -nostdlib-e _start的本质

    • -nostdlib去掉了 crt0 这个 “中间层”,让程序直接对接内核;
    • -e _start告诉链接器:“不用 crt0 的_start 了,用我自己定义的_start 作为 ELF 入口”。

这种操作的本质是跳过 C/C++ 运行时的封装,直接与操作系统内核交互,它暴露了程序执行的底层真相:内核只认 ELF 头指定的入口(默认_start),main只是运行时库为开发者提供的便捷接口


  1. crt0(C Runtime Zero)是C/C++ 程序运行时的 “初始化启动代码”,是连接操作系统内核与用户代码(如main函数)的关键中间层,默认由编译器工具链(如 GCC)自动链接,开发者通常感知不到它的存在。 ↩︎