嵌入式C语言自我修养分享课件(二)

一. 编译运行流程介绍

1.编译:编译器会将程序源代码编译成汇编代码
1).预处理:
对源代码中的伪指令进行处理(以#开头的指令)。
·删除所有的#define,展开所有宏定义。
·处理条件指令,例如 #if、#elif、#else、endif等。
·处理头文件包含指令,如 #include,将被包含的文件插入到该预编译指令的位置。
删除所有的注释
添加行号和文件名标识

2.汇编:汇编器会将汇编代码文件翻译成为二进制的机器码,保存在后缀名为.o的目标文件中,这个文件是一个 ELF 格式的文件,表示可执行可链接文件格式,例如目标文件 .o可执行文件 .exe共享目标文件 .so

3.链接:由汇编程序生成的目标文件(.o文件)不能被立即执行,还需要通过链接器,将有关的目标文件彼此相连,使所有目标文件成为一个能够被操作系统载入执行的统一整体。

某个源文件中的函数调用另一个源文件中的函数或者调用库文件中的函数时,都需要链接,否则报错函数未定义。

其中,链接处理分为静态链接和动态链接。
1.静态链接:直接在编译阶段就把静态库加入到可执行文件当中去
2.动态链接:在链接阶段只加入一些描述信息,等到程序执行时再从系统中把相应的动态库加载到内存中去。
优缺点:
静态链接:
优点:不用担心目标用户缺少库文件,在编译阶段已经和其他文件一起编译成汇编代码。
缺点:最终的可执行文件会比较大,多个应用程序之间无法共享库文件,而且库文件中的函数不一定全部用得到,造成内存浪费

动态链接:
优点:可执行文件小。
缺点:需要提前准备用户所需要的库文件。
动态链接是我们经常用到的链接方式,需要开发提前提供所需要的库文件。

4.载入:加载器会将可执行文件的代码和数据从硬盘加载到内存中,然后跳转到程序的第一条指令处开始运行。
为了避免进程所使用的内存地址相互影响,操作系统会为每个进程分配一套独立的虚拟内存地址,然后再提供一种机制,将虚拟内存地址和物理内存地址进行映射。

程序使用的内存地址叫做虚拟内存地址(Virtual Memory Address)。
实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)。

二.BSS段简介

要了解BSS段,需要先了解用户空间内存的6种不同的内存段。
从低到高为:
1.程序文件段(.text),包括二进制可执行代码。
2.已初始化数据段(.data),包括静态常量。
3.未初始化数据段(.bss),包括未初始化的静态变量。
4.堆段,包括动态分配的内存,从低地址开始向上增长。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
5.文件映射段,包括动态库、共享内存等,从低地址开始向上增长。
6.栈段,包括局部变量和函数调用的上下文等。大小一般是8MB,栈段可以通过系统调用自动地扩充空间,但是不能回收空间,所以栈段设置得太大会导致内存泄露。

内存分页:操作系统把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,叫做页,在Linux 下,每一页的大小为 4KB。
虚拟地址与物理地址之间通过页表来映射,CPU 中的 MMU (Memory Management Unit,内存管理单元)将虚拟地址转换成物理地址。

综上,一个可执行文件载入到内存需要以下几个步骤:
1.给进程分配虚拟内存空间。
2.创建虚拟地址到物理地址的映射,创建页表。
3.加载代码段和数据段等数据,即将硬盘中的文件拷贝到物理内存页中,并在页表中写入映射关系。
4.把可执行文件的入口地址写入到 CPU 的 指令寄存器(PC)中,即可执行程序。

对于未初始化的全局变量和静态局部变量,编译器将其放置在BSS 段中 。 BSS 段是不占用可执行文件存储空间,但是当程序加载到内存运行时,加载器会在内存中给BSS段开辟一段存储空间。

嵌入式C语言自我修养分享课件(二)_第1张图片
secfion header table 中会记录BSS段的大小,在符号表中会记录每个变量的地址和大小。

加载器会根据这个表的信息,在数据段的后面分配指定大小的内存空间并清零,根据符号表中各个变量的地址,在这个内存空间中给这些未初始化的全局变量、静态变量分配存储空间。

总结:BSS段的设计是为了减少文件体积。编译器对数据段和BSS 段符号的处理流程是相同的,不同点在于,在可执行文件内不给BSS段分配存储空间在程序运行时再分配存储空间和地址

三.静态链接和动态链接库

为什么需要静态链接和动态链接
一个软件项目中,为了完成特定功能,除了自定义函数,我们还可以使用别人己经封装好的函数库,例如C标准库、研发提供的库以及第三方库,库函数的使用避免了“造轮子”的重复工作,提高 了代码复用率,大大减轻了软件开发的工作量。

1.静态链接库
编译阶段,链接器将我们引用的函数代码或变量,链接到可执行文件里,和可执行程序组装在一起,这种库称为静态库。

静态库的制作和使用都很简单,使用AR命令就可以将多个目标文件打包为一个静态库。

举例:
新建一个test.c文件
嵌入式C语言自我修养分享课件(二)_第2张图片
新建一个main.c文件
嵌入式C语言自我修养分享课件(二)_第3张图片
将test.c 一整个文件打包成静态库。ar rcs
在这里插入图片描述
ar rcs 也可以写成ar -rcs
看下可执行程序的大小
在这里插入图片描述

使用ar命令制作静态库的常用命令:
-c :建立备存文件
-r:如果指定的文件己经在库中存在,则替换它
-s :无论库是否更新都强制重新生成新的符号表
-d:从库中删除指定的文件。
-o :对压缩文档成员进行排序。
-q:向库中追加指定文件。
-L:打印库中的目标文件。
-x:解压库中的目标文件。

可以看到,main函数中只调用了add函数,但是实际上,链接器将4个函数全部组装到可执行文件中了,导致并没有完全使用所有的函数,这会让最终生成的可执行文件的体积大大增加。
使用readelf -s a.out可以查看可执行文件中所链接的函数
嵌入式C语言自我修养分享课件(二)_第4张图片
为了解决这种链接多个,只使用部分的问题,可以由有两种方法
第一种就是每个函数都建立一个.c文件,然后将多个目标文件打包。
gcc -c add.c sub.c mul.c div.c
ar rcs libtest.a add.o sub.o mul.o div.o
gcc main.c -L. -ltest
这样产生的可执行文件就只使用了add函数。
C语言标准库就是这种做法,printf()函数单独定义在printf.c里,scanf()函数单独定义在scanf.c文件中, 如果你调用了一个printf()函数,则链接器只是将printf()函数的目标文件链接到你的可执行文件中。

虽然这种方法可以减小可执行文件的体积,但是当多个程序引用相同的公共代码时,这些公共代码会多次加载到内存,浪费内存资源。尤其对于一些内存配置较低的嵌入式系统,当过多的进程并发运行时,系统就可能因为内存爆满而无法流畅运行。
为了解决这个问题,动态链接的方法就产生了。

2.动态链接库
动态链接对静态链接做了一些优化:对一些公用的代码,如库,在链接期间暂不链接,而是推迟到程序运行时再进行链接。这些在程序运行时才参与链接的库被称为动态链接库。程序运行时,除了可执行文件,这些动态链接库也要跟着一起加载到内存,参与链接和重定位过程,否则程序可能就会报未定义错误,无法运行。

动态库的文件变成了以.so为后缀。一个软件采用动态链接,版本升级时主程序的业务逻辑或框架不需要改变,只需要更新对应的.dll或.so文件就可以 了,简单方便,也避免了用户重复安装卸载软件。以上面为例,我们可以将add.c , sub.c、 mul.c、 div.c 封装成动态库libtest.so,然后在程序运行时动态加载到内存。

将每个函数都写入对应的.c文件里,然后使用以下指令
在这里插入图片描述

可以发现生成的可执行程序运行时报错了,说找不到动态库文件,这是因为动态链接库要放到/lib 、/usr/lib等系统默认的库路径下 ,否则a.out就会动态链接失败。
有两种方法可以解决
第一种是将这个.so文件拷贝到/lib或者/usr/lib下,让系统可以找到。
第二种是使用LD_LIBRARY_PATH环境变量(临时有效)。
添加后可以正常运行
在这里插入图片描述
看下可执行程序的大小
在这里插入图片描述
发现是比静态库占用的空间小一点。至于小得不多,是因为本身执行程序就很小,少了三个函数组合到可执行程序中的大小。
使用readelf -s a.out发现只链接了使用的add函数。

综上:
.a文件就是静态库文件,.so文件是动态库文件。
在小型项目函数不是很多的情况下建议使用静态库,因为将每个函数都写入对应的.c文件,不如一起写入一个.c文件中,减少了时间成本,且小型项目的可执行程序在静态库和动态库的差别不是很大。
在大型多并发的项目中,建议使用动态库,防止多个程序引用相同的公共代码时,这些公共代码会多次加载到内存,浪费内存资源,造成内存爆满,无法流畅运行。

嵌入式C语言自我修养分享课件(二)_第5张图片

你可能感兴趣的:(Linux,c语言,windows,服务器)