1、二进制形式目标模块的包。
a.o \
b.o -> abc.a---库
c.o /
2、库的分类:静态库和共享库(动态库)。
静态库:扩展名.a。库中所封装的二进制代码,在链接阶段被复制到调用模块中。
共享库:扩展名.so。库中所封装的二进制代码,在链接阶段不被复制到调用模块中,被嵌入到调用模块中的仅仅是调用函数在共享库中的相对地址。在运行时,根据这个地址动态的执行共享库代码。
3、库的构建
静态库
1、编辑源程序:编写C程序及头文件。
2、编译生成目标文件:gcc -c 所有C程序(除含有main函数的C源文件外),生成以.o为后缀名的目标文件。
3、打包生成静态库文件:ar -r libxxx.a 所有以.o为后缀名的目标文件,生成libxxx.a的静态库,其中xxx为静态库名称。
ar [参数] 静态库名 目标文件列表
参数选项
-r 将目标文件插入倒静态库中,若已存在则更新
-q 将目标文件追加到静态库尾部
-d 从静态库中删除指定的目标文件
-t 列表显示静态库中的目标文件
-x 将静态库展开为目标文件(解压缩静态库)
4、使用静态库:
1、直接法:gcc 含有main函数的C源文件 libxxx.a
2、参数法:gcc 含有main函数的C源文件 -lxxx -L静态库路径
3、环境法:将export LIBRARY_PATH=$LIBRARY_PATH:.添加到~/.bash_profile文件中去
实例:
有文件calc.c,calc.h,show.c,show.h和main.c。它们的代码如下所示:
(1)calc.c的源代码:
#include "calc.h"
int add(int x, int y){
return x + y;
}
int sub(int x, int y){
return x - y;
}
(2)calc,h的源代码:
#ifndef __CALC_H__
#define __CALC_H__
int add(int, int);
int sub(int, int);
#endif //__CALC__
(3)show.c的源代码:
#include <stdio.h>
#include "show.h"
void show(int x, char opr, int y, int z){
printf("%d %c %d = %d\n",x,opr,y,z);
}
(4)show.h的源代码:
#ifndef __SHOW_H__
#define __SHOW_H__
void show(int, char, int, int);
#endif //__SHOW_H__
(5)main.c的源代码:
#include <stdio.h>
#include "show.h"
#include "calc.h"
int main(void){
printf("23 + 45 = %d\n",add(23,45));
printf("55 + 45 = %d\n",sub(55,45));
show(35,'+',41,35+41);
return 0;
}
(6)Makefile的内容:
result:main.c libmath.a
gcc main.c libmath.a -o result
libmath.a:show.o calc.o
ar -r libmath.a show.o calc.o
show.o calc.o:show.c calc.c
gcc -c show.c calc.c
clean:
rm *.o
有了(1)(2)(3)(4)(5)的文件后,使用make指令执行Makefile脚本文件即可生成所需要的静态库libmath.a和可执行文件result。
1、编辑源程序:编写C程序及头文件
2、编译生成目标模块:gcc -c -fpic 所有C程序(除含有main函数的C源文件外),生成以.o为后缀的目标文件。
(在-fpic中f是一个选项参数,pic是position independent code(位置无关码)的简写,可执行程序加载共享库时,可将其映射到其他地址空间的任何位置)
-fPIC 大模式,代码量大,速度慢,但是所有的平台都支持
-fpic 小模式,代码量少,速度快,但是只有少部分平台支持,是一种-fPIC的改进
3、链接成共享库:gcc -shared 所有目标文件 -o libxxx.so,生成以xxx为名的共享库文件libxxx.so。
4、使用共享库:
1、静态加载:所有的使用静态库的方法。运行时需要保证LD_LIBRARY_PATH环境变量中包含共享库所在的路径。
实例:
有文件calc.c,calc.h,show.c,show.h和main.c。它们的代码如下所示:
(1)calc.c的源代码:
#include "calc.h"
int add(int x, int y){
return x + y;
}
int sub(int x, int y){
return x - y;
}
(2)calc,h的源代码:
#ifndef __CALC_H__
#define __CALC_H__
int add(int, int);
int sub(int, int);
#endif //__CALC__
(3)show.c的源代码:
#include <stdio.h>
#include "show.h"
void show(int x, char opr, int y, int z){
printf("%d %c %d = %d\n",x,opr,y,z);
}
(4)show.h的源代码:
#ifndef __SHOW_H__
#define __SHOW_H__
void show(int, char, int, int);
#endif //__SHOW_H__
(5)main.c的源代码:
#include <stdio.h>
#include "show.h"
#include "calc.h"
int main(void){
printf("23 + 45 = %d\n",add(23,45));
printf("55 + 45 = %d\n",sub(55,45));
show(35,'+',41,35+41);
return 0;
}
(6)Makefile文件内容:
result:main.c libmath.so
gcc main.c libmath.so -o result
libmath.so:calc.o show.o
gcc -shared calc.o show.o -o libmath.so
calc.o show.o:calc.c show.c
gcc -c -fpic calc.c show.c
clean:
rm *.o
有了(1)(2)(3)(4)(5)的文件后,使用make指令执行Makefile脚本文件即可生成所需要的共享库libmath.so和可执行文件result。
A、函数原型:void *dlopen(const char *filename, int flag);
filename:如果只给共享库文件名,则通过LD_LIBRARY_PATH环境变量搜索共享库。如果给共享库路径,则按路径加载,不使用环境变量。
flag:
RTLD_LAZY 延迟加载,什么时候使用共享库就什么时候加载。
RTLD_NOW 立即加载
成功返回共享库句柄,失败返回NULL.
B、获取函数地址
void *dlsym(void *handle/*共享库句柄*/, const char *symbool/*函数名*/);成功返回函数地址,失败返回NULL。
C、卸载共享库
int dlclose(void *handle/*共享库句柄*/);成功返回0,失败返回非零。
D、获取错误信息
char *dlerror(void);
返回错误信息字符串,没有错误信息返回NULL。
实例:在已经有上面生成的共享库libmath.so的情况下,使用如下代码演示动态加载共享库:
#include <stdio.h>
#include <dlfcn.h>
typedef int (*PFUNC_CALC)(int, int);
typedef int (*PFUNC_SUB)(int, int);
typedef void (*PFUNC_SHOW)(int, char, int, int);
int main(void){
//动态加载共享库
void *handle = dlopen("./libmath.so",RTLD_NOW); //寻找当前目录下的的动态库文件libmath.so,RTLD_NOW表示立即加载
if(!handle){
printf("dlopen失败:%s\n",dlerror()); //获取错误信息
return -1;
}
//获取函数地址
PFUNC_CALC add = (PFUNC_CALC)dlsym(handle,"add");
if(!add){
printf("dlsym失败:%s\n",dlerror());
return -1;
}
PFUNC_SUB sub = (PFUNC_SUB)dlsym(handle,"sub");
if(!sub){
printf("dlsym失败:%s\n",dlerror());
return -1;
}
PFUNC_SHOW show = (PFUNC_SHOW)dlsym(handle,"show");
if(!show){
printf("dlsym失败:%s\n",dlerror());
return -1;
}
printf("23 + 45 = %d\n",add(23,45));
printf("67 - 32 = %d\n",sub(67,32));
show(34,'+',12,34+12);
show(54,'-',21,54-21);
//卸载共享库,dlclose数值减1
dlclose(handle);
return 0;
}
5、辅助工具:
nm:查看目标文件,可执行文件,静态库,共享库中符号列表。
ldd:查看可执行程序或共享库的动态依赖;但是如果采用动态加载共享库时,无法查看共享库的动态依赖。
ldconfig:事先把共享库的路径信息写到/etc/ld.so.conf配置文件中,ldconfig根据该配置文件生成/etc/ld.so.cache缓冲文件,并将该缓冲文件读入内存,提高动态库的加载效率。系统启动时会自动执行ldconfig,若修改了共享库配置,则需要手动执行该程序,更新缓冲。(只在Linux平台中起作用)
strip:通过删除符号表和调试信息给目标文件,可执行文件,库文件减肥。对可执行文件使用了strip命令,无法使用nm命令。
objdump:对机器指令(可执行文件)做反汇编。比如:objdump a.out。(黑客常用指令)
C语言中的错误处理
1、通过函数返回值表示错误
1)、返回合法值表示成功,返回非法值表示失败。
2)、返回有效指针表示成功,返回空指针(NULL/0xFFFFFFFF)表示失败。
3)、返回0表示成功,返回-1表示失败。如果有需要返回给调用者的数据,可以通过指针型参数向其输出。
4)、如果一个函数永远不会失败,也没有数据需要提供给调用者,可以没有返回值。比如exit函数。
2、通过错误码获得函数失败的原因,需要包含头文件#include <errno.h> //extern int errno;
1)、直接通过errno全局变量获取错误原因。
2)、将errno转换成一个字符串(错误信息):
(1)、包含#include <string.h>头文件,使用char *strerror(int errnum)函数输出,printf("%s\n",strerror(errno));所有的错误码都不为0,没有错误,错误码为0.
(2)、包含#include <stdio.h>头文件,使用void perror(const char *s)函数输出,perror("malloc");
(3)、errno在函数执行成功的情况下不会被修改,因此不能以errno非零作为发生错误的判断依据,除非在函数调用前人为的将其复位为0(在线程中不适用).
(4)、errno是一个全局变量,其值随时都有可能发生改变,在线程中不安全。
环境变量
1、环境表
(1)、每个进程都会接收一张环境表,是一个以NULL指针结尾的字符指针数组。
(2)、全局变量environ保存了环境表的首地址。
(3)、main函数的第三个参数就是环境变量表的首地址。
2、环境变量函数
#include <stdlib.h>
环境变量:<name>=<value>
getenv 根据name获得value
putenv 以<name>=<value>形式设置环境变量。如果name不存在,则会添加;存在则修改原来的value。
setenv 根据name设置value,若name存在,根据参数决定是否覆盖原value。
unsetenv 删除环境变量
clearenv 清空环境变量,environ = NULL
实例:
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
void printenv(){
printf("----环境变量----\n");
char **env;
for(env = environ; env && *env; env++){
printf("%s\n",*env);
}
printf("----------------\n");
}
int main(int argc, char *argv[], char *envp[]){
//添加环境变量
putenv("MYNAME=哈哈");
printenv();
//修改环境变量
putenv("MYNAME=呵呵");
printenv();
//获取环境变量
printf("%s\n",getenv("MYNAME"));
setenv("MYNAME","噢噢",0); //如果标志为0,表示存在不修改,不存在添加
printenv();
setenv("MYNAME","嗯嗯",1); //如果标志为1,表示存在就修改,不存在添加
printenv();
//删除环境变量
unsetenv("MYNAME");
printenv();
//清空环境变量
clearenv();
printenv();
printf("%p, %p\n",environ,envp);
return 0;
}
内存管理
层次结构,逐步深入(越来越底层):
用户层
(以下C++程序员要学的)
Boost/ACE/MFC/...
STL:内存分配器
C++:new,delete,析构,构造
(以下C程序员要学的)
标准C中:malloc,calloc,realloc,free
POSIX:brk,sbrk
Linux:mmap,munmap
系统层(以下嵌入式要学的)
内核:kmalloc,vmalloc
驱动:get_free_page
硬件实现
进程映像
1)、程序是保存在磁盘上的可执行文件。如a.out,ls,gcc,QQ.exe。
2)、运行程序时,需要把磁盘上的可执行文件加载到内存中(由加载器loder完成,之后调用main函数产生进程),形成进程。
3)、一个程序(文件)可以同时存在多个进程(内存)。
4)、进程在内存空间中的布局就是进程映像。从低地址到高地址依次是:代码区(text)->数据区(data)->BSS区(bss)->堆区(heap)->栈区(stack)->命令行参数和环境变量区
代码区(text):存放可执行指令,子面值常量,具有常属性且初始化的全局变量和静态变量。该区只读。
数据区(data):存放不具常属性且初始化的全局和静态变量。
BSS区(bss):存放未初始化的全局和静态变量。进程一加载此区即被清0.
堆区(heap):存放动态内存分配的变量。
栈区(stack):存放非静态局部变量。
命令行参数和环境变量区:存放命令行参数和环境变量。
注意:在堆区和栈区之间会留有一段空隙,一方面为堆和栈的增长预留空间,同时共享库、共享内存也会占用这个区域。
实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
const int const_global = 10; //常全局变量
int init_global = 10; //初始化全局变量
int uninit_global; //未初始化全局变量
int main(int argc, char *argv[]){
//常静态变量
const static int const_static = 10;
static int init_static = 10; //初始化静态变量
static int uninit_static; //未初始化静态变量
const int const_local = 10; //常局部变量
int prev_local; //前局部变量
int next_local; //后局部变量
//前堆变量
int *prev_heap = (int *)malloc(sizeof(int));
//后堆变量
int *next_heap = (int *)malloc(sizeof(int));
const char *literal = "literal"; //子面值常量
extern char **environ; //环境变量
printf("--------命令行参数和环境变量--------\n");
printf(" 环境变量:%p\n",environ);
printf(" 命令行参数:%p\n",argv);
printf("---------------栈区-----------------\n");
printf(" 常局部变量:%p\n",&const_local);
printf(" 前局部变量:%p\n",&prev_local);
printf(" 后局部变量:%p\n",&next_local);
printf("---------------堆区-----------------\n");
printf(" 后堆变量:%p\n",next_heap);
printf(" 前堆变量:%p\n",prev_heap);
printf("---------------BSS区----------------\n");
printf("未初始化的全局变量:%p\n",&uninit_global);
printf("未初始化的静态变量:%p\n",&uninit_static);
printf("--------------数据区----------------\n");
printf(" 初始化的静态变量:%p\n",&init_static);
printf(" 初始化的全局变量:%p\n",&init_global);
printf("--------------代码区----------------\n");
printf(" 常静态变量:%p\n",&const_static);
printf(" 子面值常量:%p\n",literal);
printf(" 常全局变量:%p\n",&const_global);
printf(" 函数:%p\n",main);
printf("PID = %d\n",getpid());
getchar();
return 0;
}
在64为Ubuntu Kylin 14.04下运行结果为:
知识补充:一个内存分页是4096(4k)个字节,它是一个内存映射单位。可以使用getpagesize()函数得到系统默认分页的大小。
使用size命令可以看到可执行文件在代码区(text),数据区(data)和BSS区(bss)的大小。比如size a.out。
虚拟内存
1、在32位系统中每个进程都有各自独立的4G字节的虚拟内存空间。
2、虚拟内存中的地址只有映射到实际内存中才能使用。
3、用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
4、虚拟内存到物理内存的映射由操作系统动态维护。系统管理着内存分页表(内存映射表)。
5、虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序使用比物理内存更大的地址空间。4G的进程空间分为两部分:0~3G-1为用户空间;3G~4G-1为内核空间。
6、用户空间中代码不能直接访问内核空间中的代码和数据,但是可以通过系统调用进入内核态,间接地与内核交互。
7、对内存的越权访问,或访问未建立映射的虚拟内存,将会导致段错误。比如:int *p_num = NULL; *p = 100; printf("%d\n",*p);
8、用户空间对应进程,进程一切换,用户空间随机变化。内核空间由操作系统内核使用,不会随进程切换而变换。内核空间由内核根据独立且唯一的页表init_mm.pgd进行映射,而用户空间的页表则每个进程一份。
9、每个进程的内存空间完全独立,因此在不同进程之间交换虚拟内存的地址毫无意义。
10、标准库的内存分配函数(malloc/calloc/realloc)需要用一套数据结构维护动态分配的内存,因此会分配比实际要求多12个字节的内存,用于存储某些控制信息。该信息一旦被破坏,将导致后续操作出现异常。如free会异常。
11、虚拟内存到物理内存的映射以页(4096字节)为单位。通过malloc函数首次分配内存,至少映射33页。在"/proc/进程号/maps"文件中可以查看映射表。即使通过free函数,释放掉全部动态分配的内存,最初的33页仍然保留,直到进程退出这33页才会真正被解除映射。
12、可以使用下面的函数得到系统默认的分页大小。
#include <unistd.h>
int getpagesize(vid); //返回内存页的字节数
char * pc = (char *)malloc(sizeof(char));
|<--------------------33页------------------------>|
---- --+-------+-----------+-----------------------------------+---------------------------
|1字节|控制信息| |
-------+------+------------+-----------------------------------+---------------------------
^ ^ ^ ^ ^
赋值后果: 段错误 OK 后续错误 不稳定(被后面的赋值所覆盖) 段错误