欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪
文章目录
c/c++程序形成可执行程序,需要经过三个步骤,编译、汇编、链接三个步骤,我们之前做链接时,使用的方法是将头文件、实现头文件方法的.c文件以及主文件(即包含main函数的文件)放在同一个目录下,这样子就能让多个.c文件链接,形成一个可执行文件。
但是我们的程序通常使用到库函数,而库函数的实现文件其实不会跟我们的主文件放在同一个目录下进行链接的。那么为什么我们可以轻松的与标准库进行链接呢?
首先标准库的头文件通常会在/usr/include路径下,这是linux系统的默认标准库的头文件路径。我们常用的stdio,stdlib文件都在该路径下。
所以我们经常在包含标准库的头文件时,通常是使用<>,而非"",这是因为我们使用<>包含的头文件,都是去/usr/include路径下查看。而\""\"包含的头文件,都是去同一路径下寻找头文件。
而与包含头文件的程序进行链接的,也不是什么.c文件,而是标准库封装的动态库(也有静态库,但是默认情况下是与动态库链接)。C语言标准库的路径是/usr/lib64/libc.so。
这是一个简单的c程序
c
#include<stdio.h>
int main()
{
printf("hello world\n");
}
我们编译该程序时,实际上gcc编译器会先到/usr/include/stdio.h中检查我们包含的stdio.h文件,接着链接/usr/lib64/libc.so动态库。最后形成可执行程序。
那么知道了原理,我们是不是也可以写一个自己的库?首先库分为静态库和动态库,它们的链接形式不同,我们先从静态库开始
如何写一个静态库
我们仿着<string.h>写一个<mystring.h>。也就是写一个针对字符串处理的库。首先是我们的头文件<mystring.h>。其代码如下:
c
#pragma once
typedef unsigned int uint;
uint my_strlen(const char* str);
char* my_strcpy(char* dest,const char* src);
int my_strcmp(const char* lhs,const char* rhs);
接着我们将头文件拷贝到/usr/incliude/路径下。
bash
cp mystring.h /usr/include/mystring.h
接着写一个实现<mystring.h>的文件<mystring.c>。
c
#include"mystring.h"
uint my_strlen(const char* str)
{
int cnt=0;
while((*str)!='\0')
{
str++;
cnt++;
}
return cnt;
}
char* my_strcpy(char* dest,const char* src)
{
char* cur=dest;
while(*cur=*src)
{
cur++;
src++;
}
return dest;
}
int my_strcmp(const char*lhs,const char*rhs)
{
while(*lhs++==*rhs++)
{
if(*lhs=='\0'||*rhs=='\0')
break;
}
return *lhs-*rhs;
}
我们将该.c文件编译生成.o文件,因为无论是动态库,还是静态库,其实都是将编译好的.o文件打包在一起。
bash
gcc -c myystring.c
接着我们是用ar -rc
指令将.o文件形成静态库,要注意,静态库是有文件格式的,一个静态库的格式为lib[libname].a
,即库名前要有前缀lib,以及后缀.a。我们将这个打包好的静态库命名为:mylib。因此打包静态库的指令为:
bash
ar -rc libmylib.a mystring.o
接着我们将生成好的静态库移动到系统默认的库的路径下。即/usr/lib64/路径下
bash
mv libmylib.a /usr/lib64/libmylib.a
现在属于我们自己的库就已经写好了。我们来写一个程序试试用一下吧。
c
#include<mystring.h>//我们包头文件时用的是<>
#include<stdio.h>
int main()
{
const char* str1="hello world";
char str2[255]={0};
my_strcpy(str2,str1);//mystring.h当中的函数
printf("%s\nstrltn:%d\n",str2,my_strlen(str2));//mystring.h的函数
if(my_strcmp(str2,str1)==0)//mystring.h的函数
{
printf("str1==str2\n");
}
return 0;
}
接着我们编译该程序。
c
gcc test.c -o test
此时我们惊讶的发现,这个程序的编译竟然不通过
通过分析,我们发现编译代码时发生了链接错误,诶?我们不是把静态库放在了/usr/lib64/吗?按道理gcc编译器会在这个默认路径下帮我们链接的。为什么没有呢?这是因为gcc会在该默认路径下找库进行链接,但是它只认识标准libc.a。不认识我们的libmylib.a。因此我们要指明,gcc编译时,要去用libmylib.a进行链接。指令为-l [libname]
。我们的[libname]是mylib,因此正确的指令为:
bash
gcc test.c -o test -l mylib
动态库
动态库生成不用ar指令将.o文件打包起来,而是使用gcc将方法文件(.c文件)集成生成动态库。动态库的命名形式为:lib[libname].so
,即前缀是lib,后缀为.so。
让gcc将文件编译生成动态库的选项为-shared以及-fPIC。因此正确指令如下
bash
gcc mystring.c -o libmylib.so -shared -fPIC
我们接着将动态库移动到/usr/lib64路径下。接着编译test.c
bash
gcc test.c -o test -l mylib
这时候有人可能就会问了,我记得/usr/lib64目录下不是还有一个libmylib.a的静态库吗?我怎么确定链接的是动态库而不是静态库。
我们要注意,使用gcc编译时,如果同一路径下存在静态库和动态库,默认链接的是动态库,而非静态库,如果我们非要链接静态库,需要加上-static选项。比如我们用相同代码的test.c,通过链接静态库的形式生成可执行程序test2。看看这两个程序有何不同。
bash
gcc test.c -o test2 -static -l mylib
我们发现,静态库链接的test2程序的大小足足是动态链接的test程序的100倍!可是它们的源代码明明是一样的,为什么链接结果差距这么大。这就要聊聊动静态库的特性了
动静态链接
与静态库链接的程序我们称为静态链接
,静态链接的原理,是将静态库中实现的方法,直接拷贝到程序当中,因此程序的大小会比较大,但是由于方法都拷贝到了程序中,因此程序的依赖性比较小。
而动态链接的程序,其原理并非是将动态库中的方法链接到程序当中,而是将方法的地址替换掉程序中调用动态库方法的代码,当执行到该代码时,进程会跳转到动态库中,在动态库执行方法,因此动态链接的程序的大小比较小,但是由于进程需要跳转到动态库当中,因此一旦动态库丢失或者发生损坏,链接该动态库的一切程序都会失效。
test是与libmylib.so链接的进程,当我们打开test进程时,操作系统会在内存当中创建一个对应的PCB(task_struct)。以及进程的地址空间(mm_struct),映射进程地址空间与内存空间的页表。如下图:
而test所链接的动态库,其实是会将动态库的地址,保存在共享区当中,因此当test进程被打开时,test所使用的所有动态库,都会被加载到内存当中。
当test进程,执行到libmylib.so库中的函数时,会跳转到libmylib.so当中,当函数执行完成时,会带着运行结果,又回到test进程当中,因此,如果我们将libmylib.so删除,那么当test运行时,动态库无法加载到内存当中,执行动态库中的函数时,也无法跳转到动态库上,因此与动态库链接的所有进程,都将无法运行。
由于动态库与进程息息相关,因此博主打算放在下一个章节当继续讲解进程与动态库之间的关系,以及它们的链接原理。