查看: 12|回复: 0

代码是怎样跑起来的:从一行源码,到终端打印出 “你好”

[复制链接]

1813

主题

5

回帖

5516

积分

超级版主

积分
5516
发表于 昨天 08:03 | 显示全部楼层 |阅读模式
前言

我们在编辑器敲下几行代码,点击运行,屏幕就输出文字。很多初学者只知道写代码、点运行,但并不清楚中间一整套复杂流程。
本文以 C 语言Hello World作为主线,完整走完预处理→编译→汇编→链接→程序加载→CPU 执行→系统调用输出全链路,再对比 Python、Java 的执行模式,搞懂不同语言底层的差异,理解源码到屏幕输出中间,编译器、链接器、操作系统、CPU 分别在干什么。
概念释义机器指令:CPU 硬件可以直接识别的二进制指令。汇编语言:机器指令的助记文本,方便人类阅读。目标文件 (.o):汇编输出的二进制中间文件,还不能直接运行。链接:把多个目标文件、系统库整合,解析函数符号依赖,生成可执行程序。字节码:虚拟机指令,不是 CPU 原生机器码,需要虚拟机解释或者 JIT 编译执行。系统调用:用户态程序向操作系统内核申请服务的入口,例如打印输出、读写文件。

一、C 语言完整链路:从.c源码到终端输出 Hello World

C 属于 Ahead‑of‑Time(AOT)编译型语言,在运行程序之前,就要把源代码完整翻译成 CPU 可以识别的机器码。整套流程分为预处理、编译、汇编、链接、加载执行、系统调用输出六大阶段。
1. 预处理 Preprocessing

输入:.c源代码文本文件。预处理器做文本替换工作:展开#include头文件、处理#define宏定义、删除注释、处理条件编译#ifdef,输出预处理之后的中间源码。
注意:预处理阶段不做语法检查,只是纯文本加工。
2. 编译 Compilation

把预处理完成的源码,做词法、语法、语义分析,生成抽象语法树,执行编译优化,输出汇编代码(.s文件),汇编是机器指令的人类可读助记形式。
3. 汇编 Assembly

汇编器读取汇编代码,翻译成二进制机器指令,生成可重定位目标文件(.o)。目标文件里面已经是机器码,但内部很多函数符号地址是未解析的,还不能直接运行。比如调用puts/printf,只记录了符号引用,并不知道这个函数真正在内存什么位置。
4. 链接 Linking

链接器做两件核心工作:
  • 把多个.o目标文件合并;
  • 解析外部符号依赖,对接 C 标准库等系统库,完成地址重定位,生成最终可执行文件。
链接分为静态链接、动态链接:
  • 静态链接:把库代码直接拷贝进可执行文件;
  • 动态链接:程序只记录对共享库的依赖,等到程序运行的时候,动态链接器才把共享库加载进来,节省磁盘与内存占用。
到这里磁盘上得到可执行程序,但它依旧还没有跑起来。
5. 操作系统加载,创建进程

在 Linux Shell 终端敲命令运行程序:
  • Shell 调用fork()创建子进程;
  • 子进程调用execve()系统调用,内核读取磁盘上的可执行文件,把程序镜像映射到进程虚拟内存,分配栈、堆、数据段;
  • 如果是动态链接程序,内核触发动态链接器加载需要的共享库;
  • 执行系统启动代码,再跳转执行我们写的main()函数。
同一个可执行文件,可以多次启动,生成多个互相独立的进程,每个进程拥有自己独立内存空间。
6. CPU 执行机器指令,系统调用输出文字

进程内部的线程获得 CPU 时间片,CPU 逐条执行机器指令,流水线取指‑译码‑执行。当执行到打印字符串逻辑:
  • C 标准库函数不会直接写屏幕;
  • 标准库封装,发起write 系统调用,向操作系统内核请求输出数据;
  • 内核把字节流交给终端设备,屏幕上最终打印出 “你好”;
  • 如果执行输出重定向./a.out > log.txt,内核就把数据流交给文件,终端屏幕看不到输出。
关键点:应用程序不能直接操作屏幕硬件,输出、读写文件都必须通过操作系统的系统调用完成。
二、Python、Java:不是直接编译成机器码,走字节码虚拟机路线

C 语言是直接编译到 CPU 原生机器码;Python、Java 并不会直接生成 CPU 机器码,而是先生成虚拟机字节码,字节码需要虚拟机解释或者 JIT 再翻译成本地机器码执行。
Python(CPython 官方实现)

  • 运行.py源码,内部先编译生成 CPython 字节码(保存为__pycache__/*.pyc缓存文件);
  • CPython 虚拟机的解释主循环逐条解释执行字节码;
  • 字节码≠CPU 机器指令,是专门给 Python 虚拟机设计的指令集;
  • 部分内置操作,底层会调用 C 实现的运行时,最终还是转化为机器码交给 CPU 执行。
新版 CPython 引入实验性 Copy‑and‑Patch JIT,但默认模式依旧以解释字节码为主。
Java

  • javac编译器,把 Java 源码编译成 JVM 字节码(.class文件);
  • JVM 虚拟机加载 class 字节码;
  • 一开始解释执行字节码;
  • 频繁运行的热点代码,HotSpot JIT 编译器,在运行时动态把字节码编译为 CPU 本地机器码,提升性能。
共同点:Python 虚拟机、JVM 虚拟机本身,也是普通 C/C++ 写出来的程序,最终依旧要跑在 CPU 机器指令之上。
三、三种语言执行流程简明对比

语言中间产物执行方式特点
C可执行文件(原生机器码)编译期 AOT 完整编译为机器码,操作系统直接加载运行运行速度高;编译阶段就做大量检查
Python(CPython)pyc 字节码文件虚拟机解释字节码(可选实验 JIT)源码直接跑,启动快,运行解释开销大
Javaclass JVM 字节码先解释,热点代码 JIT 即时编译生成本地机器码兼顾跨平台,热点路径性能高
四、容易混淆的知识点澄清

  • 误区:敲gcc main.c,只有一步编译
真相:gcc 是编译器驱动程序,内部依次调用预处理器、编译器、汇编器、链接器四套工具,只是对用户屏蔽中间文件。
  • 误区:字节码就是 CPU 机器码
真相:字节码是虚拟机的指令集,CPU 不能直接识别,必须依靠虚拟机做一层转换。
  • 误区:可执行文件复制到内存就立刻开始执行 main 函数
真相:内核加载程序后,会先运行 C 库启动初始化代码,再调用 main,main 不是程序最开始执行的代码。
  • 误区:print/printf 直接写屏幕硬件
真相:用户态代码无权直接操作硬件,全部要通过系统调用交给操作系统内核完成 IO。
结语

从敲下一行源码,到屏幕出现输出,完整链路可以概括:
源代码文本 →【预处理‑编译‑汇编‑链接】→磁盘上可执行文件 →操作系统 fork/execve 加载进内存创建进程 →CPU 执行机器指令 →库函数发起系统调用 →操作系统内核完成 IO 输出到终端。
C 语言直接编译成 CPU 机器码;Python、Java 则多了一层虚拟机字节码中间层。理解这套完整链路,就能看懂:编译器、链接器、虚拟机、操作系统、CPU 各自承担什么样工作,遇到编译报错、链接报错、运行时崩溃,也更容易定位问题根源。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|天翼网

相关侵权、举报、投诉及建议等,请发 E-mail:2026@typc.net

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.|晋ICP备2026008270号-1|晋公网安备14010602111293号

QQ客服返回顶部