文章目录
一. 学习并掌握可执行程序的编译、组装过程。学习任务如下:
1)阅读、理解和学习材料“用gcc生成静态库和动态库.pdf”和“静态库.a与.so库文件的生成与使用.pdf”,请在Linux系统(Ubuntu)下如实仿做一遍。
2)在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
3)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
二. Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:
阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。
三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)
一. 静态库动态库
1.1. 用gcc生成静态库和动态库
(1)先创建hello.h,hello.c,main.c三个文件。
// 程序一hello.h
#ifndef HELLO_H
#define HELLO_H
void hello(const char *name);
#endif
//程序二hello.c
#include <stdio.h>
void hello(const char *name)
{
printf("Hello %s!\n", name);
}
//程序三main.c
#include "hello.h"
int main()
{
hello("everyone");
return 0;
}
(2)编译hello.c
(3)创建静态库
(4)使用静态库
法一:
法二:
法三:
(5)创建动态库文件
其中-shared
选项指定生成动态连接库;-fPIC
表示编译为位置独立的代码;-L.
表示要链接的库在当前目录中;-lmyhello
编译器查找动态连接库时有隐含的命名规则,在名字前加lib,后面加上.so或.a来确定库的名称;
(6)使用动态库
(7)因为使用库的命令都相同,探究当静态库和动态库同时使用时,gcc命令会使用哪个库文件呢?
可以看出优先使用的动态库。
1.2. 静态库.a与.so库文件的生成与使用
程序文件如下:
//A1.c:
#include <stdio.h>
void print1(int arg){
printf("A1 print arg:%d\n",arg);
}
//A2.c:
#include <stdio.h>
void print2(char *arg){
printf("A2 printf arg:%s\n", arg);
}
//A.h
#ifndef A_H
#define A_H
void print1(int);
void print2(char *);
#endif
//test.c:
#include <stdlib.h>
#include "A.h"
int main(){
print1(1);
print2("test");
exit(0);
}
生成目标文件:
生成静态库文件:
使用静态库文件创建可执行程序:
共享库.so文件生成:
使用.so创建可执行程序:
报错解决(找不到.so文件,会去/usr/lib中找,需要移动到/usr/lib目录下):
还可直接使用gcc -o test test.c -L. -lname,来使用相应库文件。其中:
-L.
:表示在当前目录下
-lname
:name即对应库文件的名字
1.3. 改编x2x
所需文件:
//x2x.h
#ifndef X2X_H
#define X2X_H
float x2x(int a, int b);
#endif
//x2x.c
#include <stdio.h>
float x2x(int a, int b)
{
return a*b;
}
//x2y.h
#ifndef X2Y_H
#define X2Y_H
int x2y(int x, int y);
#endif
//x2y.c
#include <stdio.h>
int x2y(int x, int y)
{
return x+y;
}
//main.c
#include <stdio.h>
#include "x2x.h"
#include "x2y.h"
int main()
{
int a = 1, b = 2;
printf("a*b=%f\n",x2x(a,b));
printf("a+b=%d\n",x2y(a,b));
return 0;
}
最后输出:
各文件所占大小:
1.4. 动态处理
各文件大小:
可见动态连接生成的libfile.so为8.0k,静态libfile.a为4.0k。动态生成文件大小大于静态文件。
二 . Gcc不是一个人在战斗
2.1. Linux GCC常用命令
2.1.1. 一步编译:
test.c文件:
#include <stdio.h>
int main(void){
printf("Hello World!\n");
return 0;
}
2.1.2. 分阶段
编译实际上分四个阶段:
- 预处理(Preprocessing):
-E
生成.i文件,将头文件的内容加载进程序中。 - 编译(Compilation):
-S
对.i处理,生成.s文件。生成汇编代码。 - 汇编(Assembly):
-c
对.s处理,生成目标文件.o - 连接(Linking):
-o
将程序的目标文件和所需要的其它目标文件连接起来生成可执行文件。其它目标文件包括静态连接库和动态连接库
预处理:
编译:
汇编:
连接:
2.1.3. 多个程序文件的编译
新建一个test1.c文件。准备test1.h头文件,插入test.c中。
上面一条命令相当于如下三条:
2.1.4. 检错
gcc -pedantic illcode.c -o illcode
-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助
Linux 程序员离这个目标越来越近。或者换句话说,-pedantic 选项能够帮助程序员发现一些不符合
ANSI/ISO C 标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的
那些情况,才有可能被 GCC 发现并提出警告。
除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W
开头,其中最有价值的当数-Wall 了,使用它能够使 GCC 产生尽可能多的警告信息
gcc -Wall illcode.c -o illcode
GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优
秀的 Linux 程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将
警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror 选项,那
么 GCC 会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:
gcc -Werror test.c -o test
2.1.5. 库文件连接
开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支
持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、
或 lib、dll)的集合。。虽然 Linux 下的大多数函数都默认将头文件放到/usr/include/目录下,而库文
件则放到/usr/lib/目录下;Windows 所使用的库文件主要放在 Visual Stido 的目录下的 include 和 lib,
以及系统文件夹下。但也有的时候,我们要用的库不再这些目录下,所以 GCC 在编译时必须用自己
的办法来查找所需要的头文件和库文件。
例如我们的程序 test.c 是在 linux 上使用 c 连接 mysql,这个时候我们需要去 mysql 官网下载 MySQL
Connectors 的 C 库,下载下来解压之后,有一个 include 文件夹,里面包含 mysql connectors 的头
文件,还有一个 lib 文件夹,里面包含二进制 so 文件 libmysqlclient.so
其中 inclulde 文件夹的路径是/usr/dev/mysql/include,lib 文件夹是/usr/dev/mysql/lib
这时编译可执行文件需要:
gcc -c -I /usr/dev/mysql/include test.c -o test.o
链接:
gcc -L /usr/dev/mysql/lib -lmysqlclient test.o -o test
默认情况下,GCC会优先使用动态链接库,如果要强制使用静态链接库,则可以在编译时加上-static选项。
gcc -L /usr/dev/mysql/lib -static -lmysqlclient test.o -o test
静态链接时搜索路径顺序:
- ld 会去找 GCC 命令中的参数-L
- 再找 gcc 的环境变量 LIBRARY_PATH
- 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的
动态链接时、执行时搜索路径顺序:
- 编译目标代码时指定的动态库搜索路径
- 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
- 配置文件/etc/ld.so.conf 中指定的动态库搜索路径
- 默认的动态库搜索路径/lib
- 默认的动态库搜索路径/usr/lib
有关环境变量:
LIBRARY_PATH 环境变量:指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH 环境变量:指定程序动态链接库文件搜索路径
2.2. GCC编译器背后的故事
2.2.1 简介
- GCC:
GCC(GNU C Compiler)是编译工具。将C/C++程序转化为处理器可执行的二进制代码 - Binutils:
一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。是开发和调试不可缺少的工具
名称 | 作用 |
---|---|
addr2line | 将程序地址转换成其对应的程序源文件及所对应的代码行,也可以得到所对应的函数。帮助调试器在调试的过程中定位对于的源代码位置 |
as | 主要用于汇编 |
ld | 主要用于链接 |
ar | 主要用于创建静态库 |
ldd | 查看一个可执行程序依赖的共享库 |
objcopy | 将一种对象文件翻译成另一种格式,譬如将.bin转换成.elf、或者将.elf转换成.bin等 |
objdump | 反汇编 |
readelf | 显示有关ELF文件的信息 |
size | 列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等 |
- C运行库
C 语言标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义在 stdio 头文件中。C 语言标准仅仅定义了 C 标准库函数原型,并没有提供实现。因此,C 语言编译器通常需要一个 C 运行时库(C Run Time Libray,CRT)的支持。C 运行时库又常简称为 C 运行库。与 C 语言类似,C++也定义了自己的标准,同时提供相关支持库,称为 C++运行时库。
2.2.2 实仿
准备Hello.c程序:
#include <stdio.h>
int main(void)
{
printf("Hello World! \n");
return 0;
}
预处理:
- 将所有的#define删除,并且展开所有的宏定义,处理所有的条件预编译指令,比如#if #ifdeff #elif #else #endif等。
- 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置
- 删除所有注释
//
,/* */
- 保留所有的#pragma编译器指令,后续编译过程需要使用
编译:
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。命令如下
gcc -S hello.i -o hello.s
汇编:
汇编过程对汇编代码进行处理,生成处理器能识别的指令,保存为后缀为.o的目标文件。通过调用 Binutils 中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可。
gcc -c hello.s -o hello.o
还可使用as
Hello.o目标文件为ELF格式的可重定向文件
链接:- 静态链接
在编译阶段直接将静态库加入到可执行文件中。链接器必须要完成的任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用) - 动态链接
链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把动态库加载到内存中。 - 在Linux系统中,gcc编译链接时的动态库搜索路径的顺序通常为:首先从gcc命令的参数-L指定的路径寻找;再从环境变量LIBRARY_PATH指定的路径寻址;再从默认路径/lib、/usr/lib、/usr/local/lib寻找
- 在Linux系统中,执行二进制文件时的动态库搜索路径的顺序通常为:首先搜索编译目标代码时指定的动态库搜索路径;再从环境变量LD_LIBRARY_PATH指定的路径寻址;再从配置文件/etc/ld.so.conf中指定的动态库搜索路径;再从默认路径/lib、/usr/lib寻找
- 在Linux系统中,可以用ldd命令查看一个可执行程序依赖的共享库。
一个ELF可执行文件通常被链接为不同的段,常见的比如.text、.data、.rodata、.bss等
2.2.3 分析ELF文件
- ELF文件的段
ELF文件格式如下图所示,位于ELF Header和Section Header Table 之间的都是Section。一个典型的ELF文件包含下面几个段:
.text:已编译程序的指令代码段
.rodata:ro代表只读,只读数据
.data:已初始化的C程序全局变量和静态局部变量
.bss:未初始化的C程序全局变量和静态局部变量
.debug:调试符号表,调试器用此段的信息帮助调试。
- 反汇编ELF
参考:
https://www.cnblogs.com/li-hao/archive/2012/03/01/2376227.html
https://blog.csdn.net/kooktao/article/details/78114688?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCo