Linux-Level2-day02:静态与动态库详解;动态加载动态库方法;程序信息辅助工具;错误号和错误信息;环境变量读写操作;

1.静态库 静态库的本质就是将多个目标.o文件打包成一个文件。 链接静态库就是将库中被调用的代码复制到当前调用模块中。 使用静态库的程序通常会占用较大的空间,库中代码一旦更新修改,所有使用该库的程序必须重新ar链接进行更新一下。 使用静态库的程序在运行无需依赖库,将库删除了都可以运行,其执行效率高。用户直接下载可执行程序就行。

静态库的形式:libxxx.a 构建静态库: ar -r libxxx.a x.o y.o z.o 使用静态库: gcc ... -lxxx

如果库路径不是缺省的话,就指定路径

gcc ... -lxxx -L<库路径> 或者export LIBRARY_PATH=<库路径>gcc ... -lxxx 或者写入bash

代码演示用静态库:static/

思路:先做两个C文件,一个负责计算cal,一个负责显示show,两个h文件分别包含对应的C文件,目的是检查C文件函数里面的接口是不是正确,同时通过包含h文件方式为主文件main提供对应C文件里面的功能,也还可以单独做一个math.h文件包含其余所有的h文件,目的是用到其他文件时候不用一个个添加包含。

 

项目开发好了,提供哪些文件给用户: 需要提供calc.h show.h math.h 与 libmath.a 与main.c文件即可,C文件就不用提供了,因为文件libmath.a文件就是由.c 到.o在集合成.a文件的

2.动态(共享)库

动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码全部复制到当前程序模块中,相反被嵌入到调用程序模块中的仅仅是被调用代码在动态库中的相对地址,运行的时候再根据函数地址偏移量来找函数。如果动态库中的代码同时为多个进程所用,动态库的实例在整个内存空间中仅需一份,共享一套库代码,因此动态库也叫共享库或共享对象(Shared Object, so)。使用动态库的模块所占空间较小,不用复制代码,拷贝相对地址过来了,即使修改了库中的代码,只要接口(函数参数个数与类型)保持不变,无需重新链接,运行程序即自动更新。使用动态库的代码在运行时需要依赖库,执行效率略低。但节省内存空间啊。用ldd 命令+可执行程序就可以查看程序用到哪些动态库。动态库的形式:libxxx.so

构建动态库:(加个fpic来生成位置无关码库)

gcc -c -fpic xxx.c -> xxx.o

gcc -shared -o libxxx.so x.o y.o z.o

使用动态库:

gcc main.c -lxxx -L <库路径> -o main

代码演示用动态:shared/

 

 

修改C代码,重新生成库后,不再重新链接。运行程序一样更新了。

注意:多了一步export LD_LIBRARY_PATH=./。不然找不到库。

这里删除库就运行不了main了 如果修改C代码只需要重新生成库(.c更新了.o就要更新,集成的so也要更新),但main.c不用再次与库进行链接这个命令gcc main.c -lmath -L./ -o main省略,运行也能更新,前提是函数接口不变。再次注意两个环境变量需要指定路径,一个是链接时候,一个是运行时候。

nm+可执行程序 :查看用到的函数地址

ldd +可执行程序:查看用来哪个动态库

 

 

gcc缺省链接共享库,可通过-static选项强制链接静态库。

显然动态库远比静态库文件大小不一样,并且ldd命令查看到hello2根本没有动态库

 

上面是静态加载动态库,在运行的时候用动态库,下面讲如何通过程序编写来实现动态加载动态库。

3.动态加载动态库

解决问题:我们在运行程序时候可以动态选择使用哪些动态库,使用完成后什么时候释放动态库减少资源消耗。

#include <dlfcn.h>   //系统提供的针对动态库的动态加载函数集(非标准库c头文件)
​
1 打开动态库
成功返回动态库的句柄,失败返回NULL。
void* dlopen(const char* filename, int flag); 
​
FILE* fp = fopen(...);文件句柄,不需要知道里面有什么结构体内容
fread(fp...);你只需要知道抓住这个句柄的函数,可以对文件进行操作
fwrite(fp...);
​
filename - 动态库路径,若只给文件名没给路径,则根据LD_LIBRARY_PATH环境变量路径搜索动态库
flag - 加载方式,可取以下宏定义的值:
    RTLD_LAZY - 延迟加载(惰性优化,用到库函数变量等再从硬盘加载到内存),即使用动态中函数变量等时才加载
    RTLD_NOW  - 立即加载(立即读库函数变量等到内存)
该函数所返回的动态库句柄唯一地标识了系统内核所维护的动态库对象,将作为后续函数调用的参数。
​
 
2 获取动态库里面函数供给下面程序使用
void* dlsym(void* handle, const char* symbol);
成功返回函数地址(即函数第一条语句地址),也叫函数的指针,失败返回NULL。
handle - 动态库句柄,上个函数返回值
symbol - 符号(函数或全局变量)名
该函数所返回的函数指针是void*类型,需要强制类型转换为实际的函数指针类型才能调用。
​
 
​
3 将整个程序用完之后的动态库里面函数等关闭,但不妨碍别的程序用这个动态库里面函数
int dlclose(void* handle); 成功返回0,失败返回非零。
handle - 动态库句柄
​
4 输出错误信息调试信息函数
char* dlerror(void);
之前若有错误发生则返回错误信息字符串,否则返回NULL

演示代码:load.c

#include <stdio.h>
#include <dlfcn.h>
int main(){
    //动态加载动态库libmath.so  
     void* handle= dlopen("./shared/libmath.so", RTLD_NOW);//当前路径day02
     if (!handle){//判定是否打開失敗
        fprintf(stderr,"dlopen:%s\n",dlerror());
        return -1;
     }
​
     //在动态库中得到add函數的入口地址
     int (*add)(int,int) = (int(*)(int,int))dlsym(handle,"add");
     if (!add){//左边需要定义add指针来接返回值,右边强制转换成函数指针类型,左边也需要是函数指针变量
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
     }
    
     //在动态库中得到sub函數的入口地址
     int (*sub)(int,int) = (int(*)(int,int))dlsym(handle,"sub");
     if (!sub){
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
     }
​
     //在动态库中得到show函數的入口地址
     void (*show)(int,char,int,int) = (void(*)(int,char,int,int))dlsym(handle,"show");
     if (!show){
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
     }
​
     int a = 2,b = 3;
     show(a,'+',b,add(a,b));
     show(a,'-',b,sub(a,b));
​
    if (dlclose(handle)){
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
    }
​
    return 0;
}
​

 

通过ldd查看没有math动态库,因为它运行时候通过我们编写的函数找库函数,不是之前动态库静态链接的方式,这样程序灵活度更大,例如你在加载库的时候没有加载成功,可以加载另外一个替代者的库,备用方案,如果你用静态方式加载,你有一个库加载不成功整个进程启动不了,所以动态加载还可以通过报错方式提示你,不成功就换另外一个库。

四、辅助工具

1.查看符号表命令:nm

作用:列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的一些符号

代码:nm.c

int a = 0; // 全局变量
static int b = 1; // 静态全局变量
void foo(void) {
    int c = 2; // 局部变量
    static int d = 3; // 静态局部变量
    printf("Hello, World!\n");
}

 

发现没有c局部变量,因为只有程序运行才会在栈开辟内存给这个变量,因此可目标文件里面没有这个变量。

同时变量赋予初值会影响变量的存放在内存中区域位置

  1. 显示二进制模块的反汇编信息命令:objdump -S

作用:将二进制变成汇编码,反着来。

 

3.Strip命令:删除目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号表和调试信息:

作用:减少文件大小,或者防止你看到我用到哪些符号表信息等

4.ldd命令:查看可执行程序文件或动态库文件所依赖的动态库文件

五、错误号和错误信息

1.通过函数的返回值表达错误

返回整数的函数:通过返回合法值域以外的值表示错误
int age(char const* name) {
  ...
  return 1000;
​
}
​
返回指针的函数:通过返回NULL指针表示错误
不需要通过返回值输出信息的函数:返回0表示成功,返回-1表示失败。
int delete(char const* filename) {
  ...
  return 0;
  ...
  return -1;
}
​

如何知道出错误类型?

2.通过错误号(系统定义好的全局变量)和错误信息表示产生错误的具体原因

#include <errno.h> 头文件帮你外部变量声明好了,里面包含所有错误号。
全局变量:errno,整数,标识最近一次系统调用发生的错误
​
#include <string.h>
char* strerror(int errnum); // 根据错误号返回错误信息,需要你自己打印,参数是错误号errno
​
#include <stdio.h>
void perror(const char* s); // 直接打印最近错误的错误信息,参数是自己写的提示
​
printf函数的%m标记被替换为最近错误的错误信息

代码:errno.c

#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void) {
    FILE* fp = fopen("none", "r");
    if (!fp) {
        printf("fopen: %d\n", errno);
        printf("fopen: %s\n", strerror(errno));
        perror("fopen出错:");
        printf("fopen: %m\n");
        return -1;
    }
    // ...其他可能出错的地方进行编写错误提示
    fclose(fp);
    return 0;
}

 

虽然所有的错误号都不是0,但是因为在函数执行成功的情况下错误号全局变量errno不会被清0,因此不能用errno是否为0作为函数成功失败的判断条件,是否出错还是应该根据函数的返回值来决定。

返回值 = 函数调用(...); 
if (返回值表示函数调用失败) {
  根据errno错误号判断发生了什么错误
  针对不同的错误提供不同的处理
}

代码:iferr.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
    
    FILE* fp = fopen("none", "r");
    if (!fp) {
        perror("fopen出错啦");//文件打开失败,产生一个错误号errno
        return -1;
    }
    
    int* pi = malloc(sizeof(int));
//  if (errno) {//不能以错误号判断是否发生错误
    if (!pi) {//函数返回值判断是否发生错误
        perror("malloc出错,程序退出");//做出错误处理:例如打印错误信息并且结束程序
        return -1;
    }
    // ...
    free(pi);
    fclose(fp);
    return 0;
}

六、环境变量

每个进程都有一张独立的环境变量表,通过指针访问,其中的每个条目都是一个形如“键=值”形式的环境变量。

终端直接输入env命令:查看显示shell进程的环境变量

全局变量:environ(二级字符指针)系统帮你定义好的变量,没有在头文件声明,需要自己在代码做外部声明。

environ->| * |->AAA=aaa\0

| * |->BBB=bbb\0

| * |->CCC=ccc\0

.......

|NULL|

上面符号解释:所谓环境变量表就是一个以NULL指针结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个以空字符结尾的字符串,该字符串就是形如”键=值”形式的环境变量。

#include <stdio.h>
#include <stdlib.h>
void penv(char** env) {
    printf("---- 打印本程序中环境变量 ----\n");
    while (env && *env)
        printf("%s\n", *env++);
    printf("---------------------------\n");
}
int main(int argc, char* argv[], char* envp[]) {//main有三个参数
    extern char** environ;
    printf("%p %p\n", environ, envp);//地址相同即相同变量
    penv(environ);//main第三个参数就是environ即环境变量表的首地址
    //penv(envp);//与上面打印一样内容
    return 0;
}

​​​​​​​

 

1 根据环境变量名获取其值
char* getenv(char const* name);
成功返回变量名匹配的变量值,失败返回NULL。
name - 环境变量名,即等号左边的部分
​
2添加或修改环境变量
int putenv(char* string);
成功返回0,失败返回-1。
string - 形如“键=值”形式的环境变量字符串
若其键已存在,则修改其中,若其键不存在,则添加新变量
​
3 添加或修改环境变量(功能与上面相似)
int setenv(const char* name, const char* value,int overwrite);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
value - 环境变量值,即等号右边的部分
overwrite - 当name参数所表示的环境变量名已存在,此参数取0则保持该变量的原值不变,若此参数取非0,则将该变量的值修改为value。
​
4 删除指定环境变量
int unsetenv(const char* name);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
​
5清空所有环境变量
int clearenv(void);
成功返回0,失败返回-1。

代码:env1.c

#include <stdio.h>
#include <stdlib.h>
​
void penv(char** env) {
    printf("---- 打印本程序中环境变量 ----\n");
    while (env && *env)
        printf("%s\n", *env++);
    printf("---------------------------\n");
}
​
int main(void) {
    extern char** environ;
    printf("HOME=%s\n", getenv("HOME"));
    if (putenv("NAME=dpq666") == -1) {//放入环境变量
        perror("用putenv函数失败");
        return -1;
    }
    penv(environ);//打印放入的环境变量
   /* 
    if (setenv("NAME", "dpq6666dpq666", 0) == -1) {//功能与上面一样
        perror("setenv函数错误");
        return -1;
    }
    penv(environ);
    */
    
    if (unsetenv("NAME") == -1) {//删除环境变量name
        perror("用unsetenv函数失败");
        return -1;
    }
    penv(environ);
    
    if (clearenv() == -1) {//删除所有环境变量
        perror("clearenv");
        return -1;
    }
    printf("环境变量表删除后的环境变量指针environ地址:%p\n", environ);//空地址
    penv(environ);
    return 0;
}
​

环境变量作用:用于进程之间相互传递信息,读取与写入环境变量实现进程控制。

七、内存

1.虚拟内存、物理内存

虚拟内存:地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。

物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。

虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。

2 物理内存包括半导体内存和换页文件两部分。

当半导体内存(内存条)不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中(硬盘磁盘),这叫页面换出(内存数据存入硬盘),一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存中,这叫页面换入(内存读取硬盘)。因此,系统中的虚拟内存比半导体内存大得多。

3.进程映射(Process Maps)

每个进程都拥有独立的4G字节的虚拟内存(因为是虚拟的各个进程不会冲突),分别被映射到不同的物理内存区域。内存映射和换入换出都是以页为单位,1页=4096字节。4G虚拟内存中高地址的1G被映射到内核的代码和数据区,这1个G在各个进程间共享(因此严格可以说有3G独立的)。用户的应用程序只能直接访问低地址的3个G虚拟内存,因此该区域被称为用户空间,而高地址的1个G虚拟内存则被称为内核空间。用户空间中的代码只能直接访问用户空间3G的数据,如果要想访问内核空间中的代码和数据必须借助专门的系统调用完成。

用户空间的3G虚拟内存可以进一步被划分为如下区域:

---------------------------------------------------------------------------
           
           系统内核(1G):内核的代码和数据区,唯有此区各个进程间共享
​
高地址----------------------------------------------------------------------
                        
                        栈区:非静态局部变量
                        命令行参数和环境变量  
                        
----------------------------------------------------------------------
                              v
​
                           可以活动边界
​
                              ^
----------------------------------------------------------------------
​
                     堆区:动态内存分配(malloc函数族)
                     
----------------------------------------------------------------------
                
                    BSS区:无初值的全局和静态局部变量
                    
----------------------------------------------------------------------
​
                 数据区:非const型有初值的全局和局部静态变量
​
----------------------------------------------------------------------
​
                         代码区:存放可执行指令
              只读常量:字符串,等等字面值常量,const型有初值的全局和静态局部变量
                      
​
低地址----------------------------------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dpq666dpq666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值