Linux下的C语言开发
GCC、glibc和GNU C的关系
GCC
GCC 全称GNU Compiler Collection,主要是一套编译器工具集,支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada、Go和D等。
GCC 的主要作用是将源代码编译成机器语言,生成可执行文件或库文件。它也提供了一些优化选项,可以在编译过程中优化代码,提高程序运行的效率。
glibc
glibc,全称GNU C Library,是C语言标准库的一个实现版本,为C语言提供了标准的API,包括输入输出处理、字符串操作、内存管理等。glibc是Linux 系统上最常用的C标准库实现之一,它实现了C标准规定的所有标准库函数以及POSIX(可移植操作系统接口)的扩展。
GNU C
GNU C 通常指的是GNU项目的C语言编程标准,特别是在GCC中实现的C语言的扩展
和特性。
三者之间的关系
GCC 使用glibc 作为其C语言程序的标准库。当GCC编译C语言程序时,程序中使用的标准库函数(如printf或malloc)是通过glibc提供的。
GNU C 是GCC中实现的C语言的一个版本,包含了对C语言标准的支持以及GNU特有的扩展。这些扩展可以在使用GCC编译程序时通过特定的编译选项启用。
总的来说,GCC是编译器,负责将源代码转换为可执行代码;glibc是运行时库,提供程序运行所需的标准函数和操作系统服务的接口;而GNU C则定义了GCC支持的C语言的标准和扩展。
这三者共同构成了GNU/Linux系统下开发和运行C语言程序的基础。
POSIX
POSIX,全称为“可移植操作系统接口”(Portable Operating SystemInterface),是一组标准,用来确保各种不同的操作系统能够提供相同的应用编程接口(API)。这套标准由 IEEE(电气和电子工程师协会)制定,标识符为IEEE 1003。
POSIX 标准的主要目的是促进应用软件与多种类型的操作系统之间的兼容性。通过遵循POSIX 标准,开发人员可以编写能够在各种不同系统上运行的程序,而无需对程序进行大量修改。这包括Unix、Linux、MacOS以及其他类Unix系统。
主要内容包括:
(1)系统调用和库:定义了操作系统应提供的核心服务,如文件系统操作、进程管理和线程控制。
(2)Shell和工具:规定了标准命令行接口和一系列基本工具,如awk、echo等。
(3)程序接口:包括语言、函数库等接口规范,使程序能够在任何遵循POSIX的操作系统上运行。
C语言编译过程
预处理
在C语言编译过程中,预处理是其中的第一个阶段,它的主要目的是处理源代码文件中的预处理指令,将它们转换成编译器可以识别的形式。
预处理主要包含宏替换、文件包含、条件编译、注释移除等几种任务。
预处理的输出通常是经过预处理后的源代码文件,它会被保存成一个临时文件,并作为编译器的输入。预处理器处理后的文件通常会比原始源文件大,因为它会展开宏和包含其他文件的内容。
预处理命令
superdaray@ubuntu:~/helloworld$ gcc-E hello.c-o hello.i
superdaray@ubuntu:~/helloworld$ gcc-E main.c-o main.i
-E:Expand(展开)的缩写,该参数指定gcc执行预处理操作。
-o:选项用于指定输出文件的名称,默认情况下,编译器将生成一个.out的可执行文件
.i:intermediate(中间的)的缩写,预处理后的源文件通常以.i作为后缀。
编译
编译阶段,编译器会将经过预处理的源代码文件转换成汇编代码。在这个阶段,编译器会将源代码翻译成机器能够理解的中间代码。
包括词法分析、语法分析、语义分析和优化等过程。
编译器会检查代码的语法和语义,生成对应的汇编代码。编译阶段是整个编译过程中最复杂和耗时的阶段之一,它对源代码进行了深入的分析和转换,确保了程序的正确性和性能。
编译命令
superdaray@ubuntu:~/helloworld$ gcc-S hello.i-o hello.s
superdaray@ubuntu:~/helloworld$ gcc-S main.i-o main.s
-S:Source(源代码)的缩写,该参数指定gcc将预处理后的源码编译为汇编语言。
.s:Assembly Source(汇编源码)的缩写,通常编译后的汇编文件以.s作为后缀。
汇编
汇编阶段是C语言编译过程中的重要阶段,它将编译器生成的中间代码或汇编代码转换成目标机器的机器语言代码,也就是目标代码。这个阶段由汇编器(Assembler)完成。
其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务:符号解析、指令翻译、地址关联、重定位、代码优化。
最终,汇编器会将翻译和处理后的目标代码输出到目标文件中,用于后续的链接和生成可执行程序或共享库文件。
汇编命令
superdaray@ubuntu:~/helloworld$ gcc-c main.s-o main.o
superdaray@ubuntu:~/helloworld$ gcc-c hello.s-o hello.o
-c:可以被理解为CompileorAssemble(编译或汇编),该参数可以指定gcc将汇
编代码翻译为机器码,但不做链接。此外,该参数也可以用于将.c文件直接处理为机器码,
同样不做链接。
.o:Object的缩写,通常汇编得到的机器码文件以.o为后缀。
.o文件解读
查看汇编文件命令
superdaray@ubuntu:~/helloworld$ objdump-s main.o
main.o:
文件格式 elf64-x86-64-64
Contents of section .text:
0000 f30f1efa 554889e5 b8000000 00e80000 ....UH..........
0010 0000b800 0000005d c3 .......].
Contents of section .comment:
0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11
0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22
0020 2e303429 2031312e 342e3000 .04) 11.4.0.
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 19000000 00450e10 8602430d .........E....C.
0030 06500c07 08000000 .P......
文件大致可以分为五个部分:
(1)文件格式
main.o:
文件格式 elf64-x86-64-64
(2).text节
Contents of section .text:
0000 f30f1efa 554889e5 b8000000 00e80000 ....UH..........
0010 0000b800 0000005d c3 .......].
① 列之间是以空格分隔的,左侧第一列为四位16进制数,用于表示当前行的地址偏移量,上述文件中,.text节第一列第一行为0x0000,表示这一行的地址偏移量是从0开始的,第一列第二行为0x0010,表示这一行的偏移量是从十进制的16开始的。
(1字节8位,1个16进制的数是四位,每行32个16进制的数,即16字节)
② 从第二至第五列共4列均为16进制数表示的机器码,一行写满,刚好占用16个字节,因此,第一行的地址从0开始,第二行从16开始。可以看到,main函数源码处理之后得到的机器码共占用了25个字节的空间。
③ 第六列即最后一列是机器码的ASCII码表示,和②中的16进制表示相对应。对于ASCII 码无法表示的字符,全部用.表示,对于.text节,这部分是无意义的,因为机器码的意义和作用与ASCII码表示无关。
(3).comment节
Contents of section .comment:
0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11
0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22
0020 2e303429 2031312e 342e3000 .04) 11.4.0.
这部分包含编译器和编译选项的信息,用于记录编译这个文件的环境。
(4).note.gnu.property 节
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
通常包含了一些GNU特定的属性。
(5).eh_frame节
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 19000000 00450e10 8602430d .........E....C.
0030 06500c07 08000000 .P......
包含了用于异常处理的元数据,如每个函数的堆栈信息,可用于异常处理和调试。
.o文件section介绍
除了上面的四个section,.o文件中还可能存在其它section。通过以下方式查看更多的section 类型。
可执行文件需要经过链接才可以获得
superdaray@ubuntu:~/helloworld$ gcc main.o hello.o-o main
查看可执行文件main中的所有section及其头部信息
superdaray@ubuntu:~/helloworld$ objdump-h main
输出如下
列
每个section对应两行,第一行为section的布局信息,如下。
这些信息以空格分隔,共有七列,如下。
① Idx (Index): 节的索引号,这是一个简单的序列号,用于唯一标识每个section。
② Name: 节的名称,如.text、.data、.bss等。每个名称代表了该节的用途或包含的数据类型。
③ Size: 节的大小,以十六进制表示。这表示节在文件中占用的字节数。
④ VMA (Virtual Memory Address): 虚拟内存地址,指出当程序加载到内存中时,该节的内容应放置在内存的什么位置。这个地址是虚拟的,用于运行时。了解即可。
⑤ LMA (Load Memory Address): 加载内存地址,指出节内容在可执行文件中的实际位置。通常与VMA相同,除非创建位置独立的代码。了解即可。
⑥ File off (File Offset): 文件偏移,以十六进制表示,指出该节在文件中的起始位置。
⑦ Algn (Alignment): 对齐,表示该节在内存中的对齐要求。例如,2**3表示该节的数据在内存中的地址应该是8的倍数。
以.section 为例,改节的索引为15,名称为.text,大小为0x116,虚拟内存地址和加载内存地址均为0x1060,文件偏移为0x1060,内存中的地址是以2的四次方即16字节对齐的,与objdump-s main.o看到的内容一致。
属性
第二行为该节的标志或属性信息,如下。
CONTENTS:该section在文件中包含实际的数据或代码。
ALLOC:在程序执行时,该section需要被分配内存空间。
LOAD:该section的内容需要从磁盘加载到内存中。
READONLY:该section是只读的,不能被程序修改。
DATA:指示该section包含数据而非执行代码,这通常用于区分包含变量和常量的section。
CODE:指示该section包含可执行代码。
WRITE:该section在运行时可以被写入,通常与DATA标志一起出现,表明这是
一个包含可修改数据的section,如全局变量。
EXECINSTR:该section包含可执行指令。
常见section
除了执行objdump-s main.o 看到的几种section,常见的还有:.data,.bss
和.rodata
① .data:包含初始化了的全局变量和静态变量。这些变量在程序开始执行前就已经被赋予了初始值。
② .bss(Block Started by Symbol):包含未初始化的全局变量和静态变量。对于ELF文件,.bss节并不真正占用文件空间,它仅仅是一个占位符,指示程序启动时需要分配多少空间并将其清零。
③ .rodata(Read-Only Data):包含只读数据,比如字符串常量和其他程序中用到的不可修改的数据。
反汇编
可以执行下面的指令对main.o内容进行反汇编:
superdaray@ubuntu:~/helloworld$ objdump-d main.o
反汇编内容保留了objdump-s main.o看到的前两节内容,主要是将.text节的内容反汇编为汇编代码。
链接
链接阶段,由链接器完成。链接器将各个目标文件以及可能用到的库文件进行链接,生成最终的可执行程序。在这个阶段,链接器会解析目标文件中的符号引用,并将它们与符号定义进行匹配,以解决符号的地址关联问题。链接器还会处理全局变量的定义和声明,解决重定位问题,最终生成可执行文件或共享库文件。
通常,C语言的链接共有三种方式:静态链接、动态链接和混合链接。三者的区别就在于链接器在链接过程中对程序中库函数调用的解析。
静态链接
superdaray@ubuntu:~/helloworld$ gcc-static main.o hello.o-o main
-static:该参数指示编译器进行静态链接,而不是默认的动态链接。使用这个参数,GCC 会尝试将所有用到的库函数直接链接到最终生成的可执行文件中,包括C标准库(libc)、数学库(libm)和其他任何通过代码引用的外部库。
动态链接
库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为它们共享系统级的库代码。与静态链接不同,库代码不包含在可执行文件中。
方式一
superdaray@ubuntu:~/helloworld$ gcc main.o hello.o-o main
没有添加-static关键字,gcc默认执行动态链接,即glibc库文件没有包含到可执
行文件中。
方式二
我们也可以将自己编写的部分代码处理为动态库。
执行下面的指令将hello.o编译为动态链接库libhello.so。
superdaray@ubuntu:~/helloworld$ gcc-fPIC-shared-o libhello.so
hello.o
-fPIC:这个选项告诉编译器为“位置无关代码(PositionIndependentCode)”生成输出。在创建共享库时使用这个选项是非常重要的,因为它允许共享库被加载到内存中的任何位置,而不影响其执行。这是因为位置无关代码使用相对地址而非绝对地址进行数据访问和函数调用,使得库在被不同程序加载时能够灵活地映射到不同的地址空间
-shared:这个选项指示GCC生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用,节省了内存和磁盘空间。
-olibhello.so:这部分指定了输出文件的名称。-o选项后面跟着的是输出文件的名字,这里命名为libhello.so。按照惯例,Linux下的共享库名称以 lib开头,扩展名为.so(表示共享对象)。
hello.o:这是命令的输入文件,即之前编译生成的目标文件。在这个例子中,GCC会将hello.o中的代码和数据打包进最终的共享库libhello.so中。
上述命令的作用是:使用GCC,采用位置无关代码的方式,从hello.o目标文件创建一个名为libhello.so 的动态共享库文件。
使用动态链接库编译新的可执行文件
superdaray@ubuntu:~/helloworld$ gcc main.o-L ./-lhello-o main_d
-L./:指定了库文件搜索路径。-L选项告诉链接器在哪些目录下查找库文件,./表示当前目录。这意味着在链接过程中,链接器将会在当前目录下搜索指定的库文件。
-lhello:指定了要链接的库。-l选项后面跟库的名称,这里是hello。根据约定,链接器会搜索名为libhello.so(动态库)或libhello.a(静态库)的文件来链接。链接器会根据-L选项指定的路径列表查找这个库。
当前目录下只有libhello.so而没有libhello.a,因此,这条命令的最终效果是动态链接当前目录下的libhello.so库以及默认的glibc库,生成可执行文件main_d。
这时如果我们直接执行main_d文件,会收到以下报错:
superdaray@ubuntu:~/helloworld$ ./main_d
./main_d: error while loading shared libraries: libhello.so:
cannot open shared object file: No such file or directory
这句报错的意思时main_d在执行过程中,没有找到动态链接库文件libhello.so文件,链接失败无法执行。Linux的默认动态链接库文件夹是/lib 和/usr/lib,而我们的libhello.so 不在其中,所以我们需要在执行的时候指明额外的动态链接库文件夹。
superdaray@ubuntu:~/helloworld$ LD_LIBRARY_PATH=/home/superdaray/helloworld ./main_d
Hello world!
混合链接
某些库静态链接,而其他库动态链接。这种方式结合了静态链接和动态链接的优点。
执行下面的指令可以将hello.o编译为静态链接库libhello.a
superdaray@ubuntu:~/helloworld$ ar crv libhello.a hello.o
ar:归档命令,用于处理静态库文件。
crv:ar命令的选项,由三个字符组成,每个字符代表一个选项:
c:创建归档文件。如果指定的归档文件不存在,ar会创建它。
r:替换归档文件中现有的文件或者向归档文件中添加新文件。如果hello.o已经在libhello.a中,它会被新版本替换;如果不存在,则会被添加。
v:详细模式(verbosemode),在处理文件时显示详细信息。使用这个选项,ar 会列出它正在执行的操作,包括哪些文件被添加或替换。
libhello.a:要创建或更新的静态库文件的名称。按照惯例,Linux下的静态库文件名以lib开头,并以.a作为文件扩展名。
hello.o:输入文件,即要添加到静态库libhello.a中的目标文件。此处只有一个目标文件hello.o,但ar命令支持同时指定多个文件。
利用静态库文件生成可执行的main文件:
superdaray@ubuntu:~/helloworld$ gcc main.o-L ./-lhello-o main
-L./表示额外的库文件位置为当前目录;
-lhello表示链接libhello.a文件。注意这里要去掉开头的lib前缀和结尾的.a后缀。编译完成后的main文件同样可以执行,并且不依赖于静态库libhello.a。
静态链接libhello.a生成的main比main_d文件要大一些,这是因为hello 库的代码被复制到了可执行文件main中,和动态链接相比,执行速度略高,但是二进制代码的复用性差,略微增加了二进制文件的体积。
需要注意的是,虽然我们静态链接了libhello.a库,但是main文件在执行时依然需要动态链接glibc的库。因此,这种方式实质上并非静态链接,而是混合链接。
gblic 的动态库和静态库
glibc 的动态库和静态库分别位于/usr/lib/x86-64_64-linux-gnu/目录下的libc.so 和 libc.a 文件中。
Makefile 基础
Makefile 是一种用于管理和自动化软件编译过程的文本文件。它通常包含了一系列规则,这些规则描述了如何根据源代码文件生成可执行文件或者其他目标文件。Makefile的核心概念是规则和依赖关系,规则定义了如何生成一个或多个目标文件,而依赖关系则指定了生成目标文件所需要的源文件或其他依赖文件。
为main.c和hello.c编写基本Makefile
文件内容如下:
# Makefile内容通常由以下部分组成
# <目标>: <前置依赖>
# <需要执行的命令>
#放在第一个的是默认目标
#目标为编译出main文件,依赖main.o和hello.o文件
#编译的命令为gcc-o main hello.o main.o
main: hello.o main.o
gcc -o main hello.o main.o
# main.o目标依赖main.c hello.h
#编译命令为gcc-c main.c
main.o: main.c hello.h
gcc -c main.c
# hello.hello.c hello.h
#编译命令为gcc-c hello.c
hello.o: hello.c hello.h
gcc -c hello.c
# clean目标可以清理编译的临时文件
clean:
rm main main.o hello.o
需要注意的是,Makefile中每个规则的命令必须以一个制表符(tab)开始,而不能是空格。否则会提示“缺失分隔符”
上文提到,gcc的-c参数不仅可以将汇编代码转换为机器码,还可以直接将C语言源文件转换为机器码,gcc-c main.c就是第二种用法,这里省略了-o main.o。默认情况下,在指定-c参数时,gcc会将与源文件名去掉扩展名再加上后缀.o作为目标文件的名称。
引入变量
Makefile 中为了方便,可以引入临时变量:
# 定义变量objects
objects := hello.o\
main.o
# 在目标中引入变量
main: $(objects)
gcc -o main $(objects)
main.o: main.c hello.h
gcc -c main.c
hello.o: hello.c hello.h
gcc -c hello.c
# clean 目标中也可以引入变量
clean:
rm main $(objects)
引入make自动推导:
make 可以根据目标自动加入所需的依赖文件和命令。例如main.o目标,会默认将main.c 作为依赖加入,同时也可以自动推导出编译main.o的命令,于是我们的Makefile 就可以改成以下内容:
objects := hello.o\
main.o
main: $(objects)
gcc -o main $(objects)
# 利用make的自动推导
clean:
rm main $(objects)
要注意的是,虽然这种方式精简Makefile的内容,但是当没有显式声明的依赖文件发生更改时Make无法追踪。
例如此时Makefile工具无法检测到hello.h的更新。
将Makefile恢复为以下内容。
#Makefile内容通常由以下3部分组成
#<目标名称>:<前置依赖>
#\t<需要执行的命令>
#定义变量objects
objects:=hello.o\
main.o
main.o:hello.h
hello.o:hello.h
clean:
rm main $(objects)
引入伪目标
伪目标并不代表实际的文件名,它们更多的是行为或动作的标识符。伪目标并不生成具体文件。
.PHONY目标
① .PHONY 是Makefile 中一个特殊的目标,用于声明其它目标是伪目标。
② 语法:.PHONY:<伪目标名称>
③ 目标为clean的规则没有前置依赖,这是因为它是用来执行清理操作的,并不是要生成名为clean的文件,因此不需要前置依赖。我们可以将clean 声明为伪目标。
将某些不生成目标文件的行为或动作(如清理、安装)声明为伪目标可以确保无条件执行规则下的命令。即便执行make命令时当前目录下存在与目标同名的文件,依然可以得到我们期望的效果。
例如如果目录下有与clean同名的文件存在,我们就需要先声明伪目标clean命令才可以正确执行
.PHONY: clean
注意事项
忽略错误
clean:
-rm main $(objects)
在rm命令前加-即可忽略错误。 rm前面的-告诉make,如果该命令执行失败,不要停止执行剩余的过程,即忽略错误。
目标名和命令中输出文件名的关系
make 输出的文件名取决于规则下的命令(即gcc语句最后-o的文件名),而目标名称(第一条语句冒号前的文件名)决定make追踪的目标文件名。如果二者不一致,make就会认为目标文件不存在而不断执行命令。我们应确保命令生成的目标文件名和目标名一致。