LV02-03-编译与调试-04-Linux系统下的HelloWorld

本文主要是编译与调试——Linux系统下的Hello World相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本

说明:此篇笔记若有更新,系统环境可能会发生变化,但大概的应该都是一样的。

PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.6的64位版本
VMware® Workstation 17 Pro 17.0.0 build-20800274
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
点击查看本文参考资料
  • 通用
参考资料 相关链接
------
点击查看相关文件下载
------

一、踏入C语言世界的第一个程序

还记得我们进入C语言世界的第一个程序吗:

1
2
3
4
5
6
7
8
9
10
#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中的动态连接器来完成。 避免静态链接的那种低效的开发方式,这是裸机开发难以做到的。