必知必会基础知识第 1 弹
时间:2021-08-19 16:23:10
手机看文章
扫描二维码
随时随地手机看文章
[导读]关注「Linux大陆」,选择「星标公众号」一起进步!大家好,我是ZhengN。Linux大陆是我的另一个公众号,主要分享一些Linux相关的知识。大家可以关注关注。为什么重新申请了一个公众号?1、留言功能。大杂烩公众号无留言功能,错失了很多与大家互动的机会。Linux大陆具备留言...
关注「Linux大陆」,选择「星标公众号」一起进步!大家好,我是ZhengN。Linux大陆是我的另一个公众号,主要分享一些Linux相关的知识。大家可以关注关注。为什么重新申请了一个公众号?
1、留言功能。大杂烩公众号无留言功能,错失了很多与大家互动的机会。Linux大陆具备留言功能,欢迎大家来踩。2、内容划分。嵌入式大杂烩主要以嵌入式为中心展开,Linux大陆以Linux为中心展开。做一些区分,方便大家进行选择筛选。好了,废话不多说~必知必会系列分享一些基础但有用的知识,有些知识虽然不能马上运用起来,但也是有必要了解了解。这是必知必会系列第一篇:GNU。本文的一部分内容之前也有在大杂烩分享过,也一并集合到这,方便大家查阅。
GNU组件及软件非常丰富,如:1、GCC
GCC原名为
Linux下面的标准c库不仅有这一个,如uclibc、klibc,以及上面被提到的Linux libc,但是glibc无疑是用得最多的。glibc在/lib目录下的.so文件为libc.so.6。libc 实际上是一个泛指。凡是符合实现了 C 标准规定的内容,都是一种 libc 。glibc 是 GNU 组织对 libc 的一种实现。它是 unix/linux 的根基之一。嵌入式行业里还常用 uClibc ,是一个迷你版的 libc 。4、GDB
GDB(GNU symbolic debugger)是 GNU Project 调试器。GDB 可以做四种主要的事情(以及支持这些事情的其他事情)来帮助你捕获行为中的错误:6、其它
GNU系统包括很多软件包,还包括非GNU的自由软件。具体的介绍可以上gnu官网(http://www.gnu.org/software/)上查看:以上是对GNU及其内容做了一个简单的介绍,下面对GUN相关的内容做一些实例分享:
接着调用编译器gcc进行编译,输入上一步的.i文件,输出.s汇编文件。然后调用汇编器as将.s为后缀的汇编语言文件处理生成以.o为后缀的目标文件。当所有的目标文件都生成之后,调用链接器ld来进行链接生成可执行文件或库文件。这一节我们先看生成可执行文件,下一节再看如何生成库文件。其中上图中表明的-E、-S、-c为gcc编译参数。gcc的基本用法如下:(2)编译过程
使用编译器将预处理文件(3)汇编过程
使用汇编器将汇编文件(4)链接过程
链接过程使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。命令为:2、动态、静态链接
上一节的第(4)步的链接过程分为两种。一种是静态链接,另外一种是动态链接。它们的区别如:
下面一起来使用nm工具查看目标目标文件的标号。实例代码test.c:
示例代码gdb_test.c:
1、留言功能。大杂烩公众号无留言功能,错失了很多与大家互动的机会。Linux大陆具备留言功能,欢迎大家来踩。2、内容划分。嵌入式大杂烩主要以嵌入式为中心展开,Linux大陆以Linux为中心展开。做一些区分,方便大家进行选择筛选。好了,废话不多说~必知必会系列分享一些基础但有用的知识,有些知识虽然不能马上运用起来,但也是有必要了解了解。这是必知必会系列第一篇:GNU。本文的一部分内容之前也有在大杂烩分享过,也一并集合到这,方便大家查阅。
GNU简介
GNU计划,有译为“革奴计划”,它的目标是创建一套完全自由的操作系统GNU,并且其内容软件完全以GPL方式发布。这个操作系统是GNU计划的主要目标,名称来自GNU's Not Unix!的递归缩写,因为GNU的设计类似Unix,但它不包含具著作权的Unix代码。作为操作系统,GNU的发展仍未完成,其中最大的问题是具有完备功能的内核尚未被开发成功。GNU的内核,称为Hurd,是自由软件基金会发展的重点,但是其发展尚未成熟。在实际使用上,多半使用Linux内核作为系统核心。Linux操作系统包涵了Linux内核与其他自由软件项目中的GNU组件和软件,可以被称为GNU/Linux。GNU组件及软件非常丰富,如:
1、GCC
GCC原名为GNU C语言编译器
(GNU C Compiler),只能处理C语言。但其很快扩展,变得可处理C ,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件
(GNU Compiler Collection)。2、glibc
glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。「glibc与libc的关系:」glibc 和 libc 都是 Linux 下的 C 函数库。libc 是 Linux 下的 ANSI C 函数库;glibc 是 Linux 下的 GUN C 函数库。ANSI C 函数库是基本的 C 语言函数库,包含了 C 语言最基本的库函数。这个库可以根据头文件划分为 15 个部分,其中包括:glibc是linux下面c标准库的实现,即GNU C Library。glibc本身是GNU旗下的C标准库,后来逐渐成为了Linux的标准c库,而Linux下原来的标准c库Linux libc逐渐不再被维护。Linux下面的标准c库不仅有这一个,如uclibc、klibc,以及上面被提到的Linux libc,但是glibc无疑是用得最多的。glibc在/lib目录下的.so文件为libc.so.6。libc 实际上是一个泛指。凡是符合实现了 C 标准规定的内容,都是一种 libc 。glibc 是 GNU 组织对 libc 的一种实现。它是 unix/linux 的根基之一。嵌入式行业里还常用 uClibc ,是一个迷你版的 libc 。
3、coreutils
coreutils 是GNU下的一个软件包,这个软件包中包含了很多程序,如ls、mv等程序。常用的如:4、GDB
GDB(GNU symbolic debugger)是 GNU Project 调试器。GDB 可以做四种主要的事情(以及支持这些事情的其他事情)来帮助你捕获行为中的错误:- 启动你的程序,并指定可能影响其行为的所有内容。
- 使程序在指定条件下停止。
- 检查程序停止时发生的情况。
- 更改程序中的内容,以便你可以尝试纠正一个错误的影响,然后继续学习另一个错误。
5、binutils
GNU binutils是一组二进制工具集。包含的工具有:6、其它
GNU系统包括很多软件包,还包括非GNU的自由软件。具体的介绍可以上gnu官网(http://www.gnu.org/software/)上查看:以上是对GNU及其内容做了一个简单的介绍,下面对GUN相关的内容做一些实例分享:GCC编译、链接
1、基本编译流程
使用gcc工具集将C语言源代码生成可执行程序需要经过4个步骤:预处理、编译、汇编、链接
。如:首先,调用预处理器cpp进行预处理,对源代码.c文件中的文件包含(include)、预编译语句(如宏定义define等)进行分析,生成.i文件。接着调用编译器gcc进行编译,输入上一步的.i文件,输出.s汇编文件。然后调用汇编器as将.s为后缀的汇编语言文件处理生成以.o为后缀的目标文件。当所有的目标文件都生成之后,调用链接器ld来进行链接生成可执行文件或库文件。这一节我们先看生成可执行文件,下一节再看如何生成库文件。其中上图中表明的-E、-S、-c为gcc编译参数。gcc的基本用法如下:
gcc [options] [filenames]
下面以一个实例来演示将C语言源代码生成可执行程序的过程。示例代码hello.c:#include
int main(void)
{
printf("Hello gcc\n");
return 0;
}
(1)预处理过程
使用预处理器cpp把源文件hello.c
经过预处理生成hello.i
文件,预处理用于将所有的#include头文件以及宏定义替换成其真正的内容。预处理的命令为:gcc -E hello.c -o hello.i
上述命令中-E
是让编译器在预处理之后就退出,不进行后续编译过程;-o
是指定输出文件名。预处理之后得到的仍然是文本文件。hello.i文件部分内容截图如下:(2)编译过程
使用编译器将预处理文件hello.i
编译成汇编文件hello.s
。编译的命令为:gcc -S hello.i -o hello.s
上述命令中-S让编译器在编译之后停止,不进行后续过程;-o是指定输出文件名。汇编文件hello.s是文本文件,部分内容截图如下:(3)汇编过程
使用汇编器将汇编文件hello.s
转换成目标文件hello.o
。汇编过程的命令为:gcc -c hello.s -o hello.o
上述命令中-c
、-o
让汇编器把汇编文件hello.s
转换成目标文件hello.o
。目标文件hello.o
是二进制文件。这时候我们可以使用如下命令查看hello.o的格式:file hello.o
显示的内容:hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
可以看到,hello.o是个ELF(Executable and Linking Format,可执行链接格式)格式文件。另外,hello.o是个二进制文件,使用vscode打开可能会出现乱码,可以安装一个Binary插件。部分内容截图如下:(4)链接过程
链接过程使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。命令为:gcc hello.o -o hello
综上:2、动态、静态链接
上一节的第(4)步的链接过程分为两种。一种是静态链接,另外一种是动态链接。它们的区别如:(1)静态链接
优点:代码装载速度快,执行速度略比动态链接库快。缺点:使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费。(2)动态链接
优点:生成的可执行文件较静态链接生成的可执行文件小。缺点:速度比静态链接慢;使用动态链接库的应用程序不是自完备的,需要依赖相关库。初学,理解不了?没关系,分享一个易懂的比喻:把链接过程看做我们平时学习时做笔记的过程。我们平时学习时准备一本笔记本专门记录我们的学习笔记,比如在某本书的某一页上看到一个很好很有用的知识,这时候我们有两种方法记录在我们的笔记本上,一种是直接把那一页的内容全部抄写一遍到笔记本上(静态链接);另一种是我们在笔记本上做个简单的记录(动态链接),比如写上:xxx知识点在《xxx》的xxx页。从这两种方法中我们可以很清楚地知道两种方式的特点,第一种方式的优点就是我们在复习的时候就很方便,不用翻阅其它书籍了,但是缺点也很明显,就是占用笔记本的空间很多,这种方法很快就把我们的笔记本给写满了。第二种方式的优点就是很省空间,缺点就是每当我们复习的时候,手头上必须备着相关的参考书籍,比如我们去教室复习的时候,就得背着一大摞书去复习,这样我们复习的效率可能就没有那么高了。这对应到我们的动态链接与静态链接上是不是就很好理解了。下面看看具体实例:「文件1(main.c):」#include "hello.h"
int main(void)
{
print_hello();
return 0;
}
「文件2(hello.c):」#include "hello.h"
void print_hello(void)
{
printf("hello world\n");
}
「文件3(hello.h):」#ifndef __HELLO_H
#define __HELLO_H
#include
void print_hello(void);
#endif
「① 演示动态链接」首先,将源文件生成目标文件(*.o),命令:gcc -c main.c hello.c
在Linux中,动态库的扩展名一般为.so
。我们把上面生成的hello.o
文件生成相应的动态库,命令:gcc -shared hello.o -o libhello.so
使用链接动态库的方式生成可执行程序,命令:gcc main.o -L. -lhello -o hello_d_lib_test
这里的-L.
的含义是在搜索库文件时包含当前目录,-lhello
的含义是链接名称为libhello.so
的动态库。此时,运行hello_d_lib_test程序,可能会出现如下错误:./hello_d_lib_test: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
这是因为找不到共享库文件libhello.so
,加载失败。因为一般情况下Linux会在/usr/lib
路径中搜索需要用到的库,而libhello.so
库并不在这个路径下。解决方法有如下几种:- 把这个文件拷贝至
/usr/lib
路径下。 - .配置文件/etc/ld.so.conf中指定的动态库搜索路径。
- 临时生效,可以用 LD_LIBRARY_PATH 环境变量指定。
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
这时候再次运行程序就可以正常运行了。「② 演示静态链接」静态库用ar
工具来制作。ar
是一个归档工具,用于建立、修改、提取归档文件(archive)。一个归档文件可以包含多个目标文件,也被称为静态库。在Linux下,静态库的扩展名一般为.a
。把目标文件hello.o
做成静态库,命令:ar -rv libhello.a hello.o
其中rv
参数为组合参数,其中r参数表示当建立的模块名已经存在时,则覆盖同名模块,v参数用来显示附加信息,比如被处理的文件的名字。使用链接静态库的方法生成可执行程序,命令:gcc main.o -L. -lhello -o hello_s_lib_test
删除静态库之后,可执行程序也是能正常运行的。事实上,使用链接静态库的方式生成的可执行程序与直接使用目标文件生成的可执行程序没有区别。只是经过了静态库的链接,变为了一个文件,方便于调用、移植和保存。归档工具ar可以很方便地查看和删除归档文件中的成员。查看静态库libhello.a
中的内容,命令:关于ar工具更多的命令参数可输入ar --help
进行查看。GCC工具集的使用
1、ar工具的使用
基本使用如上面静态链接中的用法。2、addr2line工具的使用
addr2line可以将地址信息转化成函数名或行数。例如,如下代码运行会产生段错误:「test.c:」#include
int main(void)
{
char *str = "hello";
str[0] = 'a';
return 0;
}
首先,编译时加上-g参数,产生调试信息。gcc test.c -g -o test
运行会产生段错误Segmentation fault (core dumped)。此时会产生相关错误系统存于系统日志中。我们可以使用如下命令查看我们当前程序的错误信息:dmesg | grep test
此时会输出类似如下信息:[ 1081.831805] test[2763]: segfault at 55f1d81186a4 ip 000055f1d811860d sp 00007ffc6fc1d080 error 7 in test_addr2line[55f1d8118000 1000]
此时借助addr2line工具可以查到产生错误的行号:addr2line -e test 55f1d81186a4
3、nm工具的使用
nm工具用于显示文件中的符号,可以用于各种ELF格式文件。ELF格式文件包括如下三种类型:nm工具的使用方式:nm [option] [file]
其中,可以使用nn --help
命令来查看支持的参数。其中,nm显示的符号类型如:其中符号类型有大小写之分,小写字母表示这个符号是局部符号,大写字母表示这个符号是全局符号。下面一起来使用nm工具查看目标目标文件的标号。实例代码test.c:
#include
static int a = 1;
static int b;
void print_hello(void)
{
printf("hello\n");
}
int main(void)
{
print_hello();
}
编译之后得到可执行程序test。执行如下命令查看test中的符号:nm test
输出结果如:0000000000201010 d a
0000000000201018 b b
# 省略部分内容......
000000000000064d T main
000000000000063a T print_hello
# 省略部分内容......
从输出结果可以知道,a是一个全局符号,该符号位于已初始化数据(RW Data)部分。b也是一个全局符号,该符号位于未初始化数据(BSS)部分。main符号与print_hello符号位于代码部分。4、strip工具的使用
strip工具用于删除文件中的符号。strip工具的使用方式:strip [option] [file]
其中,可以使用strip--help
命令来查看支持的参数。我们以nm工具的演示代码来做演示。我们编译得到的可执行程序为test。没有执行strip之前,使用nm命令查看到的符号如:0000000000201010 d a
0000000000201018 b b
# 省略部分内容......
000000000000064d T main
000000000000063a T print_hello
# 省略部分内容......
使用ls -lh test
命令查看test程序的大小为:8.2k。这时候执行如下命令删除test的符号部分,输出test_strip文件:strip test -o test_strip
使用nm命令查看test_strip文件是否有符号,显示结果为:nm: test_strip: no symbols
表示test_strip没有符号。使用ls -lh test_strip
命令查看test_strip的大小为:6k。可见去掉符号表之后地程序变小了。在资源有限的系统中,可以使用这种方法为程序进行瘦身。5、readelf工具的使用
readelf工具用于显示ELF格式文件的信息。例如:readelf -h test
输出结果如:ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x530
Start of program headers: 64 (bytes into file)
Start of section headers: 6528 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
通过输出信息可以知道文件的类型、文件的格式等信息。6、objdump工具的使用
objdump工具用于显示目标文件的信息。objdump工具的使用方式:objdump [option] [file]
如:objdump -h hello.o
输出结果如:hello.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000013 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000053 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000053 2**0
ALLOC
3 .rodata 0000000c 0000000000000000 0000000000000000 00000053 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002a 0000000000000000 0000000000000000 0000005f 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000089 2**0
CONTENTS, READONLY
6 .eh_frame 00000038 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
显示内容包含目标文件各个节的信息。7、strings工具的使用
strings工具用于查看文件中的字符串。strings工具的使用方式:strings [option] [file]
其中,可以使用strings--help
命令来查看支持的参数。实例代码test.c:#include
int main(void)
{
printf("11111\n");
printf("22222\n");
printf("33333\n");
printf("44444\n");
printf("55555\n");
}
编译之后得到可执行程序test。执行如下命令查看test中的符号:strings test
输出结果如:# 省略部分内容......
11111
22222
33333
44444
55555
# 省略部分内容......
8、objcopy工具的使用
objcopy工具用于对目标文件的内容进行转换。objcopy工具的使用方式:objcopy [option] [file]
如使用如下命令可以删除可执行程序test中的.data段输出到test_rm:objcopy test -R .data test_rm
objcopy配合-R参数的使用可以达到类似strip工具的效果,给程序进行瘦身。GDB的基本使用
GDB(GNU Debugger)是一个强大的命令行调试工具。在Linux下进行开发,gdb工具是必知必会的工具之一。首先,看一下gdb常用的命令:下面以实例来进行基本使用的演示:示例代码gdb_test.c:
#include
// 测试函数1
void test0(void)
{
int i = -1;
if (i = 0)
printf("i = %d\n", i);
else if (i = 1)
printf("i = %d\n", i);
else
printf("i = %d\n", i);
}
// 测试函数2
void test1(void)
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p =