Linux系统使用gcc生成静态库和动态库并使用

(一)、使用gcc生成静态库和动态库

1、静态库和动态库概念

        静态库和动态库的最大区别是,静态库链接的时候把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度和降低程序的耦合度。. 静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。. 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。. 无论静态库,还是动态库,都是由.o文件创建的。. 因此,我们必须将源程序hello.c通过gcc先编译成.o文件。

        静态库是一个在链接过程中采用静态链接方式链接进可执行文件中的库文件,在静态链接方式中,可执行文件会拷贝静态库中导出的接口并使其成为它的一部分。在Windows系统中它主要是以.lib为后缀,而在Linux系统中,主要以.a为后缀。

        动态库也叫做共享库,在编译时并不会将所导出的接口拷贝到可执行文件中,而是在运行时才会被程序所引用。在Windows系统中它主要是以.dll为后缀,而在Linux系统中,主要以.so为后缀。需要特别注意的是,在MSVC编译器中,Windows环境下不仅生成dll后缀文件,还会生成.lib文件,该文件此刻的作用是作为一个导入库。

2、以一个简单程序(hello)构建静态库和动态库

        *编译生成对应的子程序hello.h hello.c main.c

        

        *将对应的hello.c文件编译成.o文件

#gcc -c hello.c

静态库和动态库都是由.o构建的。需要先将其编译成功

        *由对应的.o文件创建静态库

        创建静态库
        创建静态库的工具:ar
        静态库文件命名规范:以 lib 作为前缀,是.a 文件

        ar -crv libmyhello.a hello.o

        *最后生成执行文件,调用静态库

程序中使用静态库
gcc -o hello main.c -L. -lmyhello,

注意:对于自定义的静态库,main.c 还可以放在-L.和-lmyhello 之间,否则 myhello 没有定义。
-L.:表示连接的库在当前目录。

验证静态库的特点
在删掉静态库的情况下,运行可执行文件,发现程序仍旧正常运行,表明静态库跟程序执行没有联系。同时,也表明静态库是在程序编译的时候被连接到代码中的。

        *创建动态库

创建动态库
创建动态库的工具:gcc
动态库文件命名规范:以 lib 作为前缀,是.so 文件

gcc -shared -fPIC -o libmyhello.so hello.o
shared:表示指定生成动态链接库,不可省略
-fPIC:表示编译为位置独立的代码,不可省略

        *调用动态库,生成执行文件

        gcc -o hello main.c -L. -lmyhello 或 gcc main.c libmyhello.so -o hello
再运行可执行文件 hello,会出现错误
问题的解决方法:将 libmyhello.so 复制到目录/usr/lib 中。由于运行时,是在/usr/lib 中找库文件的。mv libmyhello.so /usr/lib

  3、静态库与动态库比较

        静态库和动态库的主要区别在于编译时和运行时的行为不同,静态库在编译时被连接到目标代码中,而动态库在程序运行时才被载入。 此外,静态库会增加可执行文件的大小,而动态库可以实现共享和增量更新。

4、使用静态库和动态库创建功能函数

        在main函数代码将调用sub1和sum ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将sub1、sum目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序。

        这两个函数的功能分别是输入两个数字,并且计算两数相乘的结果。

        同样的步骤如上,具体操作如下。

        main.c、sub1.c、sum.c、sub1.h、sum.h代码如下:

        生成.o文件,创建静态库和动态库如下:

(二)gcc常用命令以及gcc编译器背后的故事

1、gcc常用命令

        GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。

GCC 原名为 GNU C语言编译器,因为它原本只能处理 C 语言,但如今的 GCC 不仅可以编译 C、C++ 和 Objective-C,还可以通过不同的前端模块支持各种语言,包括 Java、Fortran、Ada、Pascal、Go 和 D 语言等等。

GCC 的编译过程可以划分为四个阶段:预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)以及链接(Linking)。

Linux 程序员可以根据自己的需要控制 GCC 的编译阶段,以便检查或使用编译器在该阶段的输出信息,帮助调试和优化程序。以 C 语言为例,从源文件的编译到可执行文件的运行,整个过程大致如下。

        

以文件example.c为例说明它的用法
0. arm-linux-gcc -o example example.c
   不加-c、-S、-E参数,编译器将执行预处理、编译、汇编、连接操作直接生成可执行代码。
    -o参数用于指定输出的文件,输出文件名为example,如果不指定输出文件,则默认输出a.out

1. arm-linux-gcc -c -o example.o example.c
   -c参数将对源程序example.c进行预处理、编译、汇编操作,生成example.o文件
   去掉指定输出选项"-o example.o"自动输出为example.o,所以说在这里-o加不加都可以

2.arm-linux-gcc -S -o example.s example.c
   -S参数将对源程序example.c进行预处理、编译,生成example.s文件
   -o选项同上

3.arm-linux-gcc -E -o example.i example.c
   -E参数将对源程序example.c进行预处理,生成example.i文件(不同版本不一样,有的将预处理后的内容打印到屏幕上)
   就是将#include,#define等进行文件插入及宏扩展等操作。
  
4.arm-linux-gcc -v -o example example.c
加上-v参数,显示编译时的详细信息,编译器的版本,编译过程等。

5.arm-linux-gcc -g -o example example.c
-g选项,加入GDB能够使用的调试信息,使用GDB调试时比较方便。

6.arm-linux-gcc -Wall -o example example.c
-Wall选项打开了所有需要注意的警告信息,像在声明之前就使用的函数,声明后却没有使用的变量等。

7.arm-linux-gcc -Ox -o example example.c
-Ox使用优化选项,X的值为空、0、1、2、3
0为不优化,优化的目的是减少代码空间和提高执行效率等,但相应的编译过程时间将较长并占用较大的内存空间。

8.arm-linux-gcc   -I /home/include -o example example.c
-Idirname: 将dirname所指出的目录加入到程序头文件目录列表中。如果在预设系统及当前目录中没有找到需要的文件,就到指定的dirname目录中去寻找。

9.arm-linux-gcc   -L /home/lib -o example example.c

-Ldirname:将dirname所指出的目录加入到库文件的目录列表中。在默认状态下,连接程序ld在系统的预设路径中(如/usr/lib)寻找所需要的库文件,这个选项告诉连接程序,首先到-L指定的目录中去寻找,然后再到系统预设路径中寻找。

10.arm-linux-gcc –static -o libexample.a example.c

gcc在命令行上经常使用的几个选项是:
-c   只预处理、编译和汇编源程序,不进行连接。编译器对每一个源程序产生一个目标文件。

-o file  确定输出文件为file。如果没有用-o选项,缺省的可执行文件的输出是a.out,目标文件和汇编文件的输出对source.suffix分别是source.o和source.s,预处理的C源程序的输出是标准输出stdout。

-Dmacro 或-Dmacro=defn   其作用类似于源程序里的#define。例如:% gcc -c -DHAVE_GDBM -DHELP_FILE=\"help\" cdict.c其中第一个- D选项定义宏HAVE_GDBM,在程序里可以用#ifdef去检查它是否被设置。第二个-D选项将宏HELP_FILE定义为字符串“help”(由于 反斜线的作用,引号实际上已成为该宏定义的一部分),这对于控制程序打开哪个文件是很有用的。

-Umacro   某些宏是被编译程序自动定义的。这些宏通常可以指定在其中进行编译的计算机系统类型的符号,用户可以在编译某程序时加上 -v选项以查看gcc缺省定义了哪些宏。如果用户想取消其中某个宏定义,用-Umacro选项,这相当于把#undef macro放在要编译的源文件的开头。

-Idir   将dir目录加到搜寻头文件的目录列表中去,并优先于在gcc缺省的搜索目录。在有多个-I选项的情况下,按命令行上-I选项的前后顺序搜索。dir可使用相对路径,如-I../inc等。

-O   对程序编译进行优化,编译程序试图减少被编译程序的长度和执行时间,但其编译速度比不做优化慢,而且要求较多的内存。

-O2   允许比-O更好的优化,编译速度较慢,但结果程序的执行速度较快。

-g   产生一张用于调试和排错的扩展符号表。-g选项使程序可以用GNU的调试程序GDB进行调试。优化和调试通常不兼容,同时使用-g和-O(-O2)选项经常会使程序产生奇怪的运行结果。所以不要同时使用-g和-O(-O2)选项。

-fpic或-fPIC   产生位置无关的目标代码,可用于构造共享函数库。

以 上是gcc的编译选项。gcc的命令行上还可以使用连接选项。事实上,gcc将所有不能识别的选项传递给连接程序ld。连接程序ld将几个目标文件和库程 序组合成一个可执行文件,它要解决对外部变量、外部过程、库程序等的引用。但我们永远不必要显式地调用ld。利用gcc命令去连接各个文件是很简单的,即 使在命令行里没有列出库程序,gcc也能保证某些库程序以正确的次序出现。

gcc的常用连接选项有下列几个:
-Ldir   将dir目录加到搜寻-l选项指定的函数库文件的目录列表中去,并优先于gcc缺省的搜索目录。在有多个-L选项的情况下,按命令行上-L选项的前后顺序搜索。dir可使用相对路径。如-L../lib等。

-lname   在连接时使用函数库libname.a,连接程序在-Ldir选项指定的目录下和/lib,/usr/lib目录下寻找该库文件。在没有使用-static选项时,如果发现共享函数库libname.so,则使用libname.so进行动态连接。

-static   禁止与共享函数库连接。

-shared   尽量与共享函数库连接

2、gcc编译器背后的故事

        GCC编译工具链包括以下三部分:

  • Gcc-Core:GCC编译器,完成预处理和编译过程,将C代码转换为汇编
  • Binutils:包括了链接器ld,汇编器as,目标文件格式查看器readelf等一系列小工具
  • glibc:包含C语言标准库,C中常使用的printfmalloc等函数

一、Gcc-Core

我们安装好Gcc编译工具时,可以通过以下命令,查看GCC编译器的版本和安装路径!

#查看GCC版本信息
gcc -v
#查看安装路径
which gcc

Gcc-Core主要是完成预处理和编译过程,将C代码转化为汇编语言。

二、Binutils工具集

在程序开发的时候,可能不会直接调用这些工具,而是在使用Gcc编译指令的时候,由GCC编译器间接调用。

相关的工具如下:

  • as:汇编器,把汇编语言代码转换为机器码(目标文件)。
  • ld:链接器,把编译生成的多个目标文件组织成最终的可执行程序文件。
  • readelf:可用于查看目标文件或可执行程序文件的信息。
  • nm : 可用于查看目标文件中出现的符号。
  • objcopy: 可用于目标文件格式转换,如.bin 转换成 .elf 、.elf 转换成 .bin等。
  • objdump:可用于查看目标文件的信息,最主要的作用是反汇编。
  • size:可用于查看目标文件不同部分的尺寸和总尺寸,例如代码段大小、数据段大小、使用的静态内存、总大小等。

三、glibc库

我们编写C语言时,所使用的read、write、open、printf等函数,都是基于该库的。

在Ubuntu系统中,libc.so.6就是glibc的库文件,我们可以直接执行,查看版本信息。

四、ELF文件

ELF是一种文件格式,用于存储Linux程序。

ELF主要包括三种类型文件:

  • 可重定位文件(relocatable):编译器和汇编器产生的.o文件,被Linker所处理
  • 可执行文件(executable):Linker对.o文件进行处理输出的文件,进程映像
  • 共享对象文件(shared object):动态库文件.so

下面是三种类型的示例:

ELF的布局如下:

由图可以知道,ELF文件从概念上来说包括了5个部分:

  • ELF header,描述体系结构和操作系统等基本信息,指出section header table和program header table在文件的位置

  • program header table,这个是从运行的角度来看ELF文件的,主要给出了各个segment的信息,在汇编和链接过程中没用

  • section header table,这个保存了所有的section的信息,这是从编译和链接的角度来看ELF文件的

  • sections,就是各个节区

  • segments,就是在运行时的各个段

注意,经过上面解释我们可以看到,其实sections和segments占的一样的地方。这是从链接和加载的角度来讲的。左边是链接视图,右边是加载视图,sections是程序员可见的,是给链接器使用的概念,而segments是程序员不可见的,是给加载器使用的概念。一般是一个segment包含多个section。Windows的PE就没有这个program header table和section header table点都统一为section,只是在加载时会进行处理。所以program header table和section header table都是可选的。

ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。

实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

(三)、Ubuntu、stm32下的程序内存分配问题(堆栈、局部全局变量等)

1、题目要求

        编写一个C程序,重温全局变量、局部变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。
归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析。

2、全局变量和局部变量

全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件。

局部变量
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数的内部就是无效的,再使用就会报错。

二者的区别

局部变量只能在当前shell中使用,不能在当前shell的子shell或其他shell中使用。不加export的自定义变量或declare声明的自定义变量均是局部变量。全局变量它适用于当前shell以及其派生出来的任意子shell,子shell的子shell依然能够使用。子shell继承当前父shell的环境变量,并能一直传承下去,但是不可逆向传递,不管在子shell有没有使用export导出变量。

全局变量|环境变量可分为临时和永久的。临时环境变量在shell中export输出是临时的,关闭当前shell,再次打开或者换其他的shell,便会失去该变量。

要使export导出的全局变量永久生效,即关闭当前shell再次打开一个shell还能使用该变量,需要修改配置文件。

3、堆(stack)和栈(heap)的比较

(1)管理方式不同:
程序运行时,栈是由操作系统自动分配管理,无需程序员人工控制,包括函数的参数值、返回值、局部变量等。而堆空间的申请、释放都是有程序员人工控制,也因此容易产生内存泄漏。

(2)空间大小不同:
栈是向低地址扩展,是一块连续的内存区域。即栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将出现栈溢出错误。而堆是高地址扩展,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址扩展。

(3)产生碎片不同:
对于堆来说,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的内存碎片,程序的运行效率降低。而对于栈来说,分配的一定是连续的内存空间。

(4)分配方式不同:
堆都是程序中由malloc/new函数动态申请分配,有free/delete函数释放的;而栈的分配和释放是由操作系统完成的。栈的动态分配可有alloc()函数手动完成,但一般都无需手动操作,而是交给编译器自动进行申请和释放的。

(5)分配效率不同:
堆的内存分配效率比栈要低得多。因为栈是有操作系统提供的,会在底层堆栈提供支持,分配专门的寄存器存放栈的地址,包括压栈出栈也都有专门的指令执行,所以执行效率很高。而堆则是有C函数提供支持,它的机制相对复杂,例如分配一块内存,库函数会按照一定的算法在堆内存空间中搜索可用的足够大的内存空间,如果没有足够大的连续空间,则需要操作系统来重新整理堆内存,这样才有机会分到足够大小的空间,然后才返回。

4、Ubuntu(x86)系统和STM32(Keil)中编程验证

*代码实现

#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
	printf("hello");
	printf("%d",a);
	printf("\n");
}

int main( )
{   
	//定义局部变量
	int a=2;//栈
	static int inits_local_c=2, uninits_local_c;
    int init_local_d = 1;//栈
    output(a);
    char *p;//栈
    char str[10] = "yaoyao";//栈
    //定义常量字符串
    char *var1 = "1234567890";
    char *var2 = "abcdefghij";
    //动态分配——堆区
    int *p1=malloc(4);
    int *p2=malloc(4);
    //释放
    free(p1);
    free(p2);
    printf("栈区-变量地址\n");
    printf("                a:%p\n", &a);
    printf("                init_local_d:%p\n", &init_local_d);
    printf("                p:%p\n", &p);
    printf("              str:%p\n", str);
    printf("\n堆区-动态申请地址\n");
    printf("                   %p\n", p1);
    printf("                   %p\n", p2);
    printf("\n全局区-全局变量和静态变量\n");
    printf("\n.bss段\n");
    printf("全局外部无初值 uninit_global_a:%p\n", &uninit_global_a);
    printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
    printf("静态内部无初值 uninits_local_c:%p\n", &uninits_local_c);
    printf("\n.data段\n");
    printf("全局外部有初值 init_global_a:%p\n", &init_global_a);
    printf("静态外部有初值 inits_global_b:%p\n", &inits_global_b);
    printf("静态内部有初值 inits_local_c:%p\n", &inits_local_c);
    printf("\n文字常量区\n");
    printf("文字常量地址     :%p\n",var1);
    printf("文字常量地址     :%p\n",var2);
    printf("\n代码区\n");
    printf("程序区地址       :%p\n",&main);
    printf("函数地址         :%p\n",&output);
    return 0;
}

Ubuntu运行结果

keil操作

配置说明

代码

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
 
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
	printf("hello");
	printf("%d",a);
	printf("\n");
}

int main(void)
 {		
 	u16 t;  
	u16 len;	
	u16 times=0;
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 //串口初始化为115200
 	LED_Init();			     //LED端口初始化
	KEY_Init();          //初始化与按键连接的硬件接口
 	while(1)
	{
		//定义局部变量
	int a=2;
	static int inits_local_c=2, uninits_local_c;
    int init_local_d = 1;
    output(a);
    char *p;
    char str[10] = "yaoyao";
    //定义常量字符串
    char *var1 = "1234567890";
    char *var2 = "abcdefghij";
    //动态分配
    int *p1=malloc(4);
    int *p2=malloc(4);
    //释放
    free(p1);
    free(p2);
    printf("栈区-变量地址\n");
    printf("                a:%p\n", &a);
    printf("                init_local_d:%p\n", &init_local_d);
    printf("                p:%p\n", &p);
    printf("              str:%p\n", str);
    printf("\n堆区-动态申请地址\n");
    printf("                   %p\n", p1);
    printf("                   %p\n", p2);
    printf("\n全局区-全局变量和静态变量\n");
    printf("\n.bss段\n");
    printf("全局外部无初值 uninit_global_a:%p\n", &uninit_global_a);
    printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
    printf("静态内部无初值 uninits_local_c:%p\n", &uninits_local_c);
    printf("\n.data段\n");
    printf("全局外部有初值 init_global_a:%p\n", &init_global_a);
    printf("静态外部有初值 inits_global_b:%p\n", &inits_global_b);
    printf("静态内部有初值 inits_local_c:%p\n", &inits_local_c);
    printf("\n文字常量区\n");
    printf("文字常量地址     :%p\n",var1);
    printf("文字常量地址     :%p\n",var2);
    printf("\n代码区\n");
    printf("程序区地址       :%p\n",&main);
    printf("函数地址         :%p\n",&output);
    return 0;
	}	 
 }


运行结果

(四)、总结

        对于gcc的进一步学习,让我不禁感叹gcc真是一个强大的编译器。gcc不是一个人在战斗,gcc背后其实有一堆战友,他们为gcc变得强大做出了巨大的贡献。同时对gcc的一些常用命令也有了一定的了解,也是对程序的编译过程又进行了一次巩固,也有了更加深刻、清晰的理解,收获颇丰。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值