LV200-helloworld分析
一、踏入 C 语言世界的第一个程序
还记得我们进入 C 语言世界的第一个程序吗:
#include <stdio.h>
int main(int argc, const char *argv[])
{
int i = 0;
/* code */
printf("hello, world! This is a C program.\n");
return 0;
}如此简单的一个程序,却带领了无数的人进入了编程的世界。只不过越简单的事物背后往往蕴含着复杂的机制。 如果我们深入思考一个简单的“Hello World 程序”,就会发现很多问题看似简单, 但是实质上我们并没有一个非常清晰的思路,准确来说,应该大部分人脑海中都有一些模糊的印象, 但是真正深入到细节中去的时候,可能又模糊不清了。
二、裸机下的 Hello World
在单片机中,实现 Hello World 程序的步骤并不复杂,下面的这张图片基本上涵括了全部的开发过程,非常清晰:

- 第一步;进行源代码的编写,其中关键的点在于 printf 函数的实现,它需要依赖单片机的串口驱动程序。
- 第二步: 借助一些集成开发环境进行程序的编译。一键编译,无需学习编译链接相关知识。
- 第三步: 借助烧录工具烧录到具体芯片上。一键烧录,无需学习芯片的 flash 和各种各样启动方式。
- 第四步: 上电启动开发板,串口输出“Hello World”字符串。
三、Linux 系统下的 Hello World
1. 一个流程图
我们先来看 Linux 系统下,Hello World 程序的流程图,如下所示:

总体上整个程序的编译执行过程,可以按图片从上到下的顺序,分为四大部分的内容:
- 第一部分,上面浅绿色外框部分为程序的编译。
- 第二部分,浅黄色外框部分为 Linux 内核提供的服务。
- 第三部分,橘色外框部分为 glibc 库提供的服务。
- 第四部分,浅灰色外框部分,为用户程序。
2. 分析一下?
(1)预处理 hello.c, 主要是处理程序里面的文件包含、处理宏定义、条件编译。
(2)把 c 文件编译成为汇编文件(.s),其中进行了词法分析,语法分析,语义分析、生成中间代码、对代码进行优化等工作。
(3)把汇编文件(.s)编译成可重定位文件(.o)。
(4)把可重定位文件(.o)链接成为可执行文件,其中链接可分为静态链接和动态链接
- 静态链接: 在编译阶段就会把所有用到的库打包到自己的可执行程序中, 其优点是具有较好的兼容性,不依赖外部环境,但是生成的程序比较大。
- 动态链接: 在应用程序运行时,链接器去加载外部的共享库,并完成共享库和动态编译程序之间的链接。不同的程序可以共用代码库,节省内存空间。
(5)控制台输入./hello 命令后,Shell 会创建一个新的进程来执行该程序。fork()函数就是用于创建一个新的进程的。这里的进程可以先简单理解为程序的容器。
(6)exeve()函数可以理解为向上一步新建的进程,填充一个可执行程序(hello)。
(7)sys_execve()函数为 linux 系统调用, 被 exeve()函数调用,这里的系统调用可以理解为是操作系统系统开放给用户的最底层接口。
(8)do_exeve()函数是 sys_execve()函数的核心。
(9)load_elf_binary()函数会去文件系统中读取 hello 程序到内存,然后判断它是否是动态链接的可执行程序,如果不是,则进一步判断是否是静态链接的文件。
(10)ld-linux-xx.so 是 glibc 库中的动态连接器。如果 hello 程序是动态链接程序,该动态链接器会去加载共享库,并完成共享库和程序的链接工作, 然后准备真正开始执行 hell 程序。相反,如果 hello 程序是静态编译的程序,则无需再加载链接共享库,直接开始准备执行 hello 程序。这里 的两种情况分别执行之后.都会开始执行 hello 程序,_start 是程序的真正入口,而该符号在 glibc 中。也就是说程序的真正入口在 glibc。
(11)__libc_start_main()也是 glibc 中的函数,用于在执行用户程序前进行一些初始化工作。
(12)调用用户程序中的 mian()函数,开始执行 printf 打印函数。
(13)程序执行完了之后,调用 glibc 库中的_exit()函数,来结束当前进程。
整个过程粗略分析完了,对比裸机和 linux 系统下的“Hello World”, 很明显可以看到操作系统为我们做了大量的工作, 甚至为了节省内存空间,还把程序的链接这种非常基础性的工作,交给了 glibc 中的动态连接器来完成。 避免静态链接的那种低效的开发方式,这是裸机开发难以做到的。