解决以下问题:
1.程序为什么要被编译器编译了之后才可以运行?
2.编译器在把C语言程序转换成可以执行的机器码的过程中做了什么?
3.最后编译出来的可执行文件里面是什么?除了机器码还有什么?他们怎么存放的?怎么组织的?
4.#include<stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它怎么实现的?
5.不同编译器和不同平台的硬件平台,以及不同的操作系统,最终编译出来的结果是一样的吗?为什么?
6.hello world程序是怎么运行起来的?操作系统是怎么装它的?它从哪里开始执行,到哪里结束?main函数之前发生了什么?main函数结束以后又发生了什么?
7.如果没有操作系统,hello world可以运行吗?如果要在一台没有操作系统的机器上运行hello world需要什么?应该怎么实现?
8.printf是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
9.hello world程序在运行时,它在内存中是什么样子的?
1.程序为什么要被编译器编译了之后才可以运行?
通过编译器将其变成二进制文件,使得计算机可以理解。同时,C 程序通常依赖标准库和操作系统服务,这些实现分散在库文件中。链接器负责把这些库与你自己的代码在地址层面连接起来,确保所有函数调用都能找到正确的实现。
2.编译器在把C语言程序转换成可以执行的机器码的过程中做了什么?
编译器把C程序转成可执行文件,要经过四个阶段:预处理(展开宏和头文件)→ 编译(转成汇编)→ 汇编(转成机器码)→ 链接(合并目标文件、解决符号、生成可执行文件)。
3.最后编译出来的可执行文件里面是什么?除了机器码还有什么?他们怎么存放的?怎么组织的?
**最终生成的可执行文件本质上是一个按特定格式组织的二进制文件。**以Linux为例,其格式为ELF。
ELF 文件中主要包括:
- ELF头
描述文件类型、目标架构、入口地址(Entry Point)等元信息。
- 程序头表
告诉操作系统如何把文件映射到内存(代码段、数据段等)。
段内容,常见有:
代码段(.text) :存放 CPU 可执行的机器指令(如
main、printf的机器码)数据段(.data):存放已初始化的全局变量和静态变量
BSS 段(.bss):存放未初始化或初始化为 0 的全局变量(文件中不占空间,加载时分配)
只读数据段(.rodata) :如字符串常量
"hello world\n"节头表与节
主要用于链接和调试(如
.symtab符号表、.strtab字符串表)。组织方式:
ELF 文件在磁盘上按"头部 + 多个段/节"的方式线性存储;
加载时,操作系统根据程序头表,把不同段映射到进程的虚拟地址空间;
ELF 中的入口地址(Entry Point)并不指向
main,而是指向C运行库提供的入口函数(如_start);该入口函数负责初始化运行环境(栈、堆、I/O、全局构造等),然后调用
main;
main返回后,入口函数执行清理并最终通过系统调用结束进程。因此,ELF 文件不仅仅是机器码,还包含了代码、数据、地址信息和加载规则,操作系统正是依靠这些信息把程序正确地放进内存并运行起来。
4.#include<stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它怎么实现的?
#include <stdio.h>把标准 I/O 的接口声明引入当前源文件;包含进来意味着让编译器知道这些函数是"存在的、长什么样";C语言库是一组由编译器或操作系统提供的、预先编译好的函数集合,用来实现C语言标准规定的各种功能(如 I/O、字符串处理、内存管理等);C 语言库本身也是用C写的,但它最终会调用操作系统提供的系统调用接口。
5.不同编译器和不同平台的硬件平台,以及不同的操作系统,最终编译出来的结果是一样的吗?为什么?
不一样。因为不同硬件的指令集不同、不同操作系统的 ABI 和系统调用不同、不同编译器的调用约定也不同,这些因素共同决定了最终生成的二进制程序是平台相关的。
6.hello world程序是怎么运行起来的?操作系统是怎么装它的?它从哪里开始执行,到哪里结束?main函数之前发生了什么?main函数结束以后又发生了什么?
#include <stdio.h>
int main()
{
printf("hello world.\n");
return 0;
}
1.操作系统加载程序
用户在 shell 中输入
./helloshell 调用
fork + execve操作系统接收到命令,读取ELF文件,根据文件头信息将其加载到内存中,并分配进程资源。
OS跳转到程序的入口点(Entry Point),开始执行指令。
2.操作系统是怎么"装"它的?(加载过程)
操作系统加载程序的过程,其实就是解析ELF文件的过程:
读取ELF头:确认文件格式合法,找到程序头表(Program Header Table)。
创建虚拟地址空间:为进程划分内存区域(代码区、数据区、堆、栈)。
映射段到内存 :根据程序头表,把ELF中的
.text(代码)、.data(数据)等段,映射到物理内存中。注意:这里通常是按需加载(分页机制),不是一次性全部读入。设置运行环境:初始化栈指针(SP)、清零BSS段、打开stdin/stdout等。
3.它从哪里开始执行,到哪里结束?
从哪里开始:
不是从
main开始是从ELF文件头中指定的Entry Point开始。
这个地址通常指向C运行库提供的
_start函数。到哪里结束:
程序结束的标志是进程退出。
最终会执行到系统调用
exit()或_exit(),通知操作系统回收资源。4.main 函数之前发生了什么?
从操作系统启动程序到
main函数被调用前,C运行库会做大量初始化工作:
_start 入口 :CPU开始执行
_start指令。环境初始化:
初始化C运行时环境(堆、全局变量等)。
解析命令行参数和环境变量。
全局构造:
调用C++的全局/静态对象的构造函数。
执行
.init或.init_array段的代码。注册退出处理:
- 注册
atexit钩子,准备在main结束后运行。调用 main:
- 最终,CRT 准备好参数,调用
main(argc, argv)。5.main 函数结束以后又发生了什么?
main函数执行return后,并没有直接结束进程,CRT 还要做清理工作:
调用析构函数:
执行C++全局/静态对象的析构函数。
执行
.fini或.fini_array段的清理代码。运行退出回调:
- 执行
atexit注册的回调函数。调用 exit():
- CRT调用标准库的
exit()函数。系统调用退出:
exit()刷新缓冲区、关闭文件流,最后触发系统调用sys_exit,通知内核。资源回收:
- 操作系统回收进程占用的内存、文件句柄、PCB(进程控制块)等资源。
7.如果没有操作系统,hello world可以运行吗?如果要在一台没有操作系统的机器上运行hello world需要什么?应该怎么实现?
可以运行。没有操作系统时,hello world 就是一个裸机程序:需要自己写启动代码、链接脚本,直接操作硬件输出,并把程序烧进 ROM 从入口地址开始执行。
8.printf是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
C 语言专门给了三个工具来做这件事:
va_list← 一个指针
va_start← 指向第一个可变参数
va_arg← 取下一个参数你不用记宏怎么写,只需要记住:
printf 是通过"栈 + 指针偏移"把参数一个一个取出来的
printf 通过格式字符串解析栈中的不定参数,并通过系统调用或硬件操作将结果输出到终端。
9.hello world程序在运行时,它在内存中是什么样子的?
hello world 运行时,操作系统根据 ELF 文件在进程的虚拟地址空间中建立起代码段、数据段、堆和栈,并通过页表将这些虚拟地址映射到物理内存,使程序在统一的地址模型中执行。