AnyUnix-likeoperatingsystemneedsaClibrary:thelibrarywhichdefinesthe'systemcalls'andotherbasicfacilitiessuchasopen,malloc,printf,exit...TheGNUCLibraryisusedastheClibraryintheGNUsystemsandmostsystemswiththeLinuxkernel.TheGNUCLibraryisprimarilydesignedtobeaportableandhighperformanceClibrary.ItfollowsallrelevantstandardsincludingISOC11andPOSIX.1-2008.Itisalsointernationalizedandhasoneofthemostcompleteinternationalizationinterfacesknown.glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于glibc囊括了几乎所有的UNIX通行的标准,其内容包罗万象。而就像其他的UNIX系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个操作系统
Glibc主要实现的功能如下
1.string:字符串处理2.signal:信号处理3.dlfcn:管理共享库的动态加载4.direct:文件目录操作5.elf:共享库的动态加载器,也即interpreter6.iconv:不同字符集的编码转换7.inet:socket接口的实现8.intl:国际化,也即gettext的实现9.io10.linuxthreads11.locale:本地化12.login:虚拟终端设备的管理,及系统的安全访问13.malloc:动态内存的分配与管理14.nis15.stdlib:其它基本功能0x2:初始化代码和终止代码(InitializationandTerminationcode)
要明白的是,对于可执行文件、和共享对象来说,它们本质上都是可执行代码的集合体,区别仅仅在于
1.可执行ELF文件可以独立运行可执行代码,并且可以载入外部的动态共享库ELF中的代码进行执行2.共享对象自身仅仅是可执行代码的一个"集合体",需要被载入到可执行程序中得以执行不管是可执行文件的执行、还是共享对象被载入到可执行文件中,它们都有对应的"初始化代码"和"终止代码"
1.可执行文件1)初始化代码:初始化代码在用户程序开始执行前执行,即在main()函数之前执行2)终止代码:终止代码则在进程退出时执行,即程序return、exit返回的时候//可执行文件的初始化代码和终止代码由"Glibc初始化入口函数"负责执行2.共享对象1)共享对象的初始化代码(_init()函数)在可共享对象文件获得控制权之前执行2)共享对象的终止代码(_fint()函数)在共享对象被卸载之后执行//共享对象的初始化代码和终止代码由"动态连接器(ld-linux.so.2)"负责执行ELF的初始化工作和终止工作都统一由Glibc运行库负责
0x3:Linux程序运行的基本流程
RelevantLink:
2.C/C++运行库
运行时库(RuntimeLibrary运行期库),在计算机程序设计领域中,是指一种被编译器用来实现编程语言内置函数以提供该语言程序运行时(执行)支持的一种特殊的计算机程序库。这种库一般包括基本的输入输出或是内存管理等支持。它是一群支持正在运行程序的函数,与操作系统合作提供诸如数学运算、输入输出等功能,让程序员不需要"重新发明轮子",并善用操作系统提供的功能运行时库由编译器决定,以面向编程语言,提供其最基本的执行时需要
1.WindowsVisualBasicVisualBasicVirtualMachine(5.1)VisualBasicVirtualMachine(6.0)2.WindwosC/C++MicrosoftCRuntimeLibrary(7.0)MicrosoftCRuntimeLibrary(7.10)MicrosoftVisualC++2005SP1(8.0.59193)MicrosoftVisualC++2008SP1(9.0.30729)MicrosoftVisualC++2008ATLUpdatekb973924(9.0.30729.4148)MicrosoftVisualC++2010(10.0.40219)3.Windows.NET/C#.NETFramework4.JavaJVM(JavaVirtualMachine)5.LinuxC/C++GlibcRuntimeLibrary0x1:C语言运行库
任何一个C程序,它的背后都有一套庞大的代码库来支撑,以使得程序能够正常运行,这样的代码集合库称之为"运行时库(RuntimeLibrary)",而其中C语言的运行库,即被称为C运行库(CRT),这套代码库包括
1.启动与退出:1)入口函数2)入口函数所依赖的其他函数3)终止函数2.标准函数:由C语言标准规定的C语言标准库所拥有的函数实现3.I/O:I/O功能的封装和实现4.堆:堆的封装和实现5.语言实现:语言中一些特殊功能的实现6.调试:实现调试功能的代码这些运行库的组成成分中,C语言标准库占据了主要地位。C语言标准库是C语言标准化的基础函数库,例如printf、exit等都是标准库中的一部分。标准库定义了C语言中普遍存在的函数集合,程序员可以直接使用标准库中规定的函数而不用担心将代码移植到别的平台时对应的平台上不提供这个函数
ANSIC的标准库由24个C头文件组成,与许多其他语言(如java)的标准库不同,C语言的标准库非常轻量,它仅仅包含了数学函数、字符/字符串处理、I/O等基本方面,例如
Glibc(GNUCLibrary)是Linux平台下的主要运行库,MSVCRT(MicrosoftVisualCRun-time)
3.静态Glibc&&可执行文件入口/终止函数
void_start(){%ebp=0;intargc=popfromstack;char**argv=topofstack;__libc_start_main(main,argc,argv,__libc_csu_init,__libc_csu_finit,edx,topofstack);}//argv除了指向参数表外,还隐含紧接着环境变量表,这个环境变量表要在__libc_start_main里从argv内提取出来实际执行代码的函数是__libc_start_main\glibc-2.18\csu\libc-start.c
1.main函数正常返回2.程序中用exit退出//exit是进程正常退出的必经之路值得注意的是,_start和_exit的末尾都有一个hlt指令。这是因为在linux中,进程必须使用eixt系统调用结束,一旦exit被调用,程序的运行就会终止。_exit里的hlt指令是为了检测exit系统调用是否成功,如果失败,程序就不会终止,hlt指令的的作用就是强行把程序停止下来。而_start里的hlt的作用也是如此。为了预防某种没用exit就回到了_start的情况(例如被误删了__libc_main_start末尾的exit)
4.动态Glibc&&可执行文件入口/终止函数
5.静态Glibc&&共享库入口/终止函数
6.动态Glibc&&共享库入口/终止函数我们知道,可执行ELF程序的初始化工作是由Glibc来完成,而动态共享库的加载和初始化工作由"动态加载器(ld-linux-so.2)"完成动态连接器的入口是_start,在glibc/sysdeps/i386/dl-machine.h中的RTLD_START宏中定义。它首先调用_dl_start()
#defineRTLD_STARTasm("\n\.text\n\.align16\n\0:movl(%esp),%ebx\n\ret\n\.align16\n\.globl_start\n\.globl_dl_start_user\n\_start:\n\#Notethat_dl_startgetstheparameterin%eax.\n\movl%esp,%eax\n\call_dl_start\n\...\glibc-2.18\elf\rtld.c
\glibc-2.18\elf\rtld.c
\glibc-2.18\elf\dl-sysdep.c
\glibc-2.18\sysdeps\i386\dl-machine.h
#defineRTLD_STARTasm("\n\..._dl_start_user:\n\movl%eax,%edi\n\call0b\n\addl$_GLOBAL_OFFSET_TABLE_,%ebx\n\movl_dl_skip_args@GOTOFF(%ebx),%eax\n\popl%edx\n\leal(%esp,%eax,4),%esp\n\subl%eax,%edx\n\push%edx\n\"RTLD_START_SPECIAL_INIT"\n\movl_rtld_local@GOTOFF(%ebx),%eax\n\leal8(%esp,%edx,4),%esi\n\leal4(%esp),%ecx\n\movl%esp,%ebp\n\andl$-16,%esp\n\pushl%eax\n\pushl%eax\n\pushl%ebp\n\pushl%esi\n\xorl%ebp,%ebp\n\/*_start调用动态连接器的初始化函数(以调用每个共享对象的初始化代码_init()..)*/call_dl_init_internal@PLT\n\/*把动态连接器的终止函数(以调用每个共享对象的终止代码_fini)地址存入EDX寄存器以传给可执行文件*/leal_dl_fini@GOTOFF(%ebx),%edx\n\movl(%esp),%esp\n\/*跳转到可执行文件的入口处开始执行*/jmp*%edi\n\.previous\n\");动态连接器任务完成后将控制权转移给用户程序,此时用户程序才正是开始执行,整个流程如下
0x1:共享库的初始化&&终止函数
Win32下可以通过DllMain来进行初始化和终止工作,而Linux下则没有与之完全对应的函数,但可以通过一些方法模拟它的部分功能,即实现_init/_fini两个函数但是,我们直接在程序中去重写/实现这2个函数,例如:test.c
由此可见,这两个符号已经被编译器的脚手架代码占用了,我们不能再使用。这两个函数是用来初始化/销毁初始化全局变量/对象的,抢占这两个函数可能导致初始化/销毁初始化全局变量/对象出错
在C++中,我们可以使用类的构造函数和析构函数来完成初始化和终止工作,但是而C语言中,根本没有构造和析构函数,我们可以使用gcc的扩展
#include
7.静态库/共享库->编译/使用、动态加载
0x1:静态库
静态库的本质就是将多个目标文件打包成一个文件。在使用时链接静态库就是将库中被调用的代码复制到调用模块中。优点是使用静态库的代码在运行时无需依赖库,且执行效率高,缺点是静态库占用空间大,库中代码一旦修改必须重新链接
1.编译生成静态库
math.h(接口文件)
#ifndefMATH_H#defineMATH_Hdoubleadd(doublex,doubley);voidshow(doubleresult);#endifcalc.c(计算模块)
#include
#include
gcc-ccalc.cgcc-cshow.c将目标文件打包成静态库文件
2.使用静态库
main.c
#include
gccmain.clibmath.aorexportLIBRARY_PATH=静态库文件所在路径gccmain.c–lmath//选项-l用于指定静态库名or//gccmain.c–lmath–L静态库文件所在路径gccmain.c-lmath-L.0x2:共享库
共享库和静态库最大的不同就是,链接共享库并不需要将库中被调用的代码复制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码在共享库中的相对地址。如果共享库中的代码同时为多个进程所用,共享库的实例在整个内存空间中仅需一份,这正是共享的意义所在,共享库的优点是占用空间小,即使修改了库中的代码,只要接口保持不变,无需重新链接,缺点是使用共享库的代码在运行时需要依赖库,执行效率略低
1.共享库的构建
gcc-c-fpiccalc.cgcc-c-fpicshow.c//选项fpic是指生成位置无关代码,即调用代码通过相对地址标识被调用代码的位置,模块中的指令与该模块被加载到内存中的位置无关将目标文件打包成共享库文件
gcc-sharedcalc.oshow.o-olibmath.so//编译和链接也可以合并为一步完成gcc-shared-fpiccalc.cshow.c-olibmath.soPIC(PositionIndependentCode,位置无关代码)
1.调用代码通过相对地址标识被调用代码的位置,模块中的指令与该模块被加载到内存中的位置无关2.-fPIC:大模式:生成代码比较大,运行速度比较慢,所有平台都支持3.-fpic:小模式:生成代码比较小,运行速度比较快,仅部分平台支持2.共享库的使用
#include
exportLD_LIBRARY_PATH=共享库文件所在路径//exportLD_LIBRARY_PATH=$LD_LIBRARY_PATH:.0x3:动态加载
从本质上讲,通过编译器指令链接共享库也是动态加载的一种,只是这个工作是ld(共享库加载器)在背后完成的。而除此之外,我们还可以在程序代码中通过指定API实现动态加载指定共享库
1.头文件和库
1.#include
将共享库载入内存并获得其访问句柄
void*dlopen(constchar*filename,intflag);1.*filename:共享库路径,若只提供文件名,则根据LD_LIBRARY_PATH环境变量搜索2.flag:加载方式1)RTLD_LAZY:延迟加载,只有当使用到了共享库中的符号(如调用库中的函数)时才加载2)RTLD_NOW:立即加载函数返回的共享库访问句柄唯一地标识了系统内核所维护的共享库对象,将作为后续函数调用的参数
3.获取函数地址
从指定共享库中获取与给定函数名对应的函数入口地址
void*dlsym(void*handle,constchar*symbol);1.*handle:共享库访问句柄2.函数名//返回的函数指针是void*类型,需要强制转换为实际函数指针类型才能调用4.卸载共享库
从内存中卸载共享库
intdlclose(void*handle);1.*handle:共享库访问句柄关于共享库的卸载,有几点需要明白
1.所卸载的共享库未必真的从内存中立即消失,因为其他程序可能还需要使用该库2.只有所有使用该库的程序都显式或隐式地卸载了该库(例如退出),该库所占用的内存空间才会真正得到释放3.无论所卸载的共享库是否真正被释放,传递给dlclose函数的句柄参数都会在该函数成功返回后立即失效5.获取错误信息
char*dlerror(void);//如果发生错误则返回指向上一次错误的错误信息字符串的指针,否则返回NULL6.代码示例
#include
0x1:查看符号表
列出目标文件、可执行文件、静态库或共享库中的符号
0x2:反汇编
显示二进制模块的反汇编信息
objdump-Sa.out0x3:去除冗余信息
去除目标文件、可执行文件、静态库和共享库中的符号表、调试信息等