当前位置: 首页 > news >正文

「Hello World」真的从 main 开始吗?

「Hello World」真的从 main 开始吗?

一个颠覆认知的问题

初学 C 语言时,老师告诉你:程序从main函数开始执行

这句话对吗?对,但只对了一半。

如果你写过 Python,应该知道它的代码是从第一行开始逐行解释执行的。那么 C 语言呢?编译型语言的"入口"在哪里?今天就让我们从 Hello World 出发,看看main到底是不是程序的"第一行代码"。

你以为的

#include<stdio.h>intmain(){printf("Hello World!\n");return0;}

编译运行,输出Hello World!,看起来一切正常,main就是起点。

编译命令如下(-o参数指定输出文件名,hello.c是源码文件):

$ gcc-ohello hello.c $ ./hello Hello World!

实际情况

第一步:查看 ELF 文件入口

C 程序编译后生成的是 ELF(Linux)或 PE(Windows)格式的可执行文件。ELF 头中记录了一个Entry point address(入口地址),让我们看看它指向谁。

首先确保hello程序已经编译好,然后使用readelf工具来查看它的 ELF 头。readelf是 Linux 上专门用来读取 ELF 格式文件信息的工具,-h(header)参数表示显示 ELF 文件头:

$ readelf-hhello|grepEntry

真实结果:

Entry point address: 0x1080

再看看0x1080处是什么函数。这次用objdump工具,它是 Linux 上的反汇编器,-d(disassemble)参数表示将机器码反汇编成汇编指令。grep -A 20表示匹配到<_start>:后显示后续 20 行:

$ objdump-dhello|grep-A20'<_start>:'

真实结果:

0000000000001080 <_start>: 1080: f3 0f 1e fa endbr64 1084: 31 ed xor %ebp,%ebp 1086: 49 89 d1 mov %rdx,%r9 1089: 5e pop %rsi 108a: 48 89 e2 mov %rsp,%rdx 108d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 1091: 50 push %rax 1092: 54 push %rsp 1093: 45 31 c0 xor %r8d,%r8d 1096: 31 c9 xor %ecx,%ecx 1098: 48 8d 3d fe 00 00 00 lea 0xfe(%rip),%rdi # 119d <main> 109f: ff 15 33 2f 00 00 call *0x2f33(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34> 10a5: f4 hlt

入口函数叫_start,不是main

再看符号表确认。readelf -s(symbols)显示可执行文件的符号表,里面记录了所有函数名、变量名及其地址。用grep -E过滤出我们关心的几个符号:

$ readelf-shello|grep-E'main|_start|__libc'

真实结果:

26: 0000000000001080 38 FUNC GLOBAL DEFAULT 16 _start 31: 000000000000119d 60 FUNC GLOBAL DEFAULT 16 main 18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main
  • _start在地址0x1080
  • main在地址0x119d
  • __libc_start_main来自 glibc(UND = undefined,需要动态链接)

第二步:真正的启动流程

ELF 入口 → _start → __libc_start_main → 初始化 libc → 调用全局构造器 → main → exit → 全局析构器

_start做了 5 件事:

  1. 清空%ebp(标记最外层栈帧)
  2. 取出argcargv(内核已经把它们压到栈上了)
  3. 对齐栈(SSE 指令需要 16 字节对齐)
  4. 调用__libc_start_main,把main的地址传给它
  5. hlt指令(如果__libc_start_main返回,直接停机)

__libc_start_main是 glibc 的初始化函数,它负责:

  • 初始化线程子系统
  • 注册atexit()处理函数
  • 调用所有全局构造函数(__attribute__((constructor))
  • **调用main(argc, argv)
  • main的返回值调用exit()

第三步:用代码验证

把以下代码保存为demo_init.c

#include<stdio.h>__attribute__((constructor))voidbefore_main(){printf("[constructor] 我在 main 之前执行!\n");}__attribute__((destructor))voidafter_main(){printf("[destructor] 我在 main 之后执行!\n");}intmain(){printf("[main] Hello World!\n");printf("[main] main 函数地址: %p\n",main);return0;}

编译并运行:

$ gcc-odemo_init demo_init.c $ ./demo_init

这里gcc -o demo_init demo_init.c的作用是:调用 GCC 编译器读取demo_init.c源码,经过预处理、编译、汇编、链接四个阶段,生成名为demo_init的可执行文件(-o指定输出文件名)。./demo_init是执行当前目录下的demo_init程序。

运行结果:

[constructor] 我在 main 之前执行! [main] Hello World! [main] main 函数地址: 0x58ce5055c19d [destructor] 我在 main 之后执行!

constructormain之前打印,destructormain之后打印。程序的"生命线"比 main 更长。

第四步:甩开 libc,直接从 _start 运行

如果我们 **不用 **CRT(C Runtime),直接从_start开始呢?

void_start(){// 直接用内联汇编调用 write 系统调用__asm__volatile("mov $1, %%rax\n"// write 系统调用号"mov $1, %%rdi\n"// fd = stdout"lea (%0), %%rsi\n"// buf"mov $19, %%rdx\n"// count"syscall\n"::"r"("Hello from _start!\n"):"rax","rdi","rsi","rdx");// 用 exit 系统调用退出,不依赖 libc__asm__volatile("mov $60, %%rax\n"// exit 系统调用号"xor %%rdi, %%rdi\n""syscall\n":::"rax","rdi");}

要编译这个特殊的程序,不能用普通的gcc命令,需要加上-nostartfiles参数。这个参数告诉 GCC:“不要链接默认的 CRT 启动代码,我自己提供_start”。同时因为没链接 libc,我们也用不了printf——所以代码里直接用了系统调用(syscall指令)写入终端:

$ gcc-nostartfiles-onostart nostart.c $ ./nostart

编译命令分解:-nostartfiles意思是"不使用标准启动文件"(即不链接 CRT),-o nostart指定输出文件名,nostart.c是源文件。执行./nostart后输出:

Hello from _start! $ echo $? 0

不需要 main,不需要 printf,不需要 libc,直接在 _start 里用系统调用输出!echo $?用来查看上一条命令的退出码,这里返回 0 表示程序正常退出。

再看看这个程序的汇编(同样用objdump -d反汇编):

$ objdump-dnostart|grep-A20'<_start>:'
0000000000001000 <_start>: 1000: f3 0f 1e fa endbr64 1004: 55 push %rbp 1005: 48 89 e5 mov %rsp,%rbp 1008: 48 8d 05 f1 0f 00 00 lea 0xff1(%rip),%rax # 2000 <_start+0x1000> 100f: 48 89 45 f8 mov %rax,-0x8(%rbp) 1013: 48 8b 4d f8 mov -0x8(%rbp),%rcx 1017: 48 c7 c0 01 00 00 00 mov $0x1,%rax 101e: 48 c7 c7 01 00 00 00 mov $0x1,%rdi 1025: 48 8d 31 lea (%rcx),%rsi 1028: 48 c7 c2 13 00 00 00 mov $0x13,%rdx 102f: 0f 05 syscall 1031: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 1038: 48 31 ff xor %rdi,%rdi 103b: 0f 05 syscall

可以看到,没有调用__libc_start_main,没有printf包装,直接就是:

  1. 加载字符串地址到%rsi
  2. 设置%rax = 1write系统调用号)
  3. 执行syscall(陷入内核)
  4. 设置%rax = 60exit系统调用号)
  5. 再次syscall

这才是最底层的 Hello World!

第五步:查看进程内存映射

让我们更直观地看看程序各部分在内存中的位置:

#include<stdio.h>#include<stdlib.h>intglobal_data=42;intglobal_bss;constintglobal_rodata=100;voidfunc(){staticintstatic_var=0;printf(" 静态变量地址: %p\n",&static_var);}intmain(){intlocal_var=0;int*heap_var=malloc(sizeof(int));printf("main 函数地址: %p\n",main);printf("func 函数地址: %p\n",func);printf("字符串常量地址: %p\n","Hello");printf("已初始化全局变量: %p\n",&global_data);printf("未初始化全局变量: %p\n",&global_bss);printf("只读全局变量: %p\n",&global_rodata);func();printf("局部变量地址: %p\n",&local_var);printf("堆上变量地址: %p\n",heap_var);free(heap_var);return0;}

运行结果:

main 函数地址: 0x5636523ec272 func 函数地址: 0x5636523ec249 字符串常量地址: 0x5636523ed070 已初始化全局变量: 0x5636523ef010 未初始化全局变量: 0x5636523ef018 只读全局变量: 0x5636523ed004 静态变量地址: 0x5636523ef01c 局部变量地址: 0x7ffd3b507114 堆上变量地址: 0x56367e13b2a0

地址分布一目了然:

区域地址范围低/高
代码段 (main, func)0x56...低地址
只读数据 (.rodata)0x56...低地址
全局变量 (.data/.bss)0x56...低地址
堆 (malloc)0x56...低地址
栈 (局部变量)0x7ff...高地址

栈在高地址(0x7ff区域),代码和数据在低地址(0x56区域)。这就是经典的内存布局——栈从高地址向低地址生长,堆从低地址向高地址生长。

核心启示

  1. main不是程序的真实入口——它是被__libc_start_main调用的。真正的入口是_start
  2. _start是汇编写的,由编译器(GCC)自动注入到每个程序中。它负责设置运行环境,然后调用main
  3. 全局构造函数/析构函数main之前/之后执行,这意味着很多初始化工作(如 C++ 的全局对象构造)在main前已经完成了。
  4. 没有 libc 也能运行——通过直接系统调用,几行汇编就能实现完整的 Hello World。这让你明白,libc 只是一层方便调用系统服务的封装罢了。

思考题

尝试把这个 Hello World 编译成静态链接版本(-static),然后对比_start的汇编代码。看看静态链接时,_start和动态链接时有什么不同?为什么?

http://www.jsqmd.com/news/707703/

相关文章:

  • 华硕笔记本终极性能优化解决方案:GHelper完整使用指南
  • R语言机器学习算法快速验证与实战指南
  • Qwen3-4B-Thinking-2507-Gemini-2.5-Flash-Distill在计算机组成原理教学中的应用:自动生成习题与解析
  • 盘点2026年收费灵活的寻宠公司,能处理户外找狗的性价比哪家高 - 工业推荐榜
  • NCM解密终极指南:3步解锁网易云音乐加密格式,实现跨平台自由播放
  • PyTorch自动微分引擎autograd原理与实战
  • 如何选择访客机品牌?2026年4月推荐评测口碑对比知名园区安全管理繁琐 - 品牌推荐
  • HPH构造:核心部件与内部拆解
  • 华硕笔记本终极轻量化控制神器:G-Helper完全指南,告别臃肿的Armoury Crate!
  • Blender3mfFormat插件:在Blender中实现3D打印3MF格式的完美导入导出
  • 工业4.0时代设备端视觉异常检测技术解析
  • Windows桌面美学革命:TranslucentTB深度解析与实用指南
  • 【数据库】向量数据库:核心原理、主流产品(Milvus、Pinecone)、索引类型(IVF、HNSW)、RAG中的应用
  • 重庆会展行业观察|参展商家普遍面临的几大难题,太真实了✨
  • 3分钟快速上手:Iwara视频下载工具终极指南
  • 2025-2026年访客机品牌推荐:五大口碑产品评测对比顶尖访客信息登记易出错 - 品牌推荐
  • Intv_AI_MK11集成MySQL实战:构建智能数据查询与分析助手
  • 3大免费解密方案:打破网易云音乐格式限制的终极指南
  • 1.3 只培养人的公司——大多数传统企业的真实写照
  • 从MATLAB迁移到PyTorch 2.8:深度学习算法移植与性能对比指南
  • 英雄联盟智能助手Seraphine:三步轻松提升你的游戏胜率
  • 3分钟快速上手:baidupankey百度网盘提取码智能查询终极指南
  • AI技能赋能Docusaurus文档工程:从配置管理到智能协作
  • 丹青幻境部署教程:Z-Image Atelier与Gradio双前端部署及性能对比
  • LobeChat快速上手指南:3步搭建专属AI助手,支持通义千问和本地模型
  • 时间序列预测:Box-Jenkins方法与ARIMA模型详解
  • 3秒获取百度网盘提取码:这款开源工具如何让你的资源下载效率提升200倍?
  • 规范说明:MyBatis\-Plus 链式查询/更新/删除
  • Asian Beauty Z-Image Turbo 5分钟快速部署:本地东方美学AI绘画工具一键启动
  • Powkiddy A20安卓掌机硬件解析与模拟器优化指南