链接装载与库:第十一章——运行库
文章目录
- 一、入口函数和程序初始化
- 1.1 程序从main函数开始吗
- 1.2 入口函数如何实现
- 1.3 运行库与IO
- 二、C/C++运行库
- 2.1 C语言运行库
- 2.2 C语言标准库
- 2.3 glibc
一、入口函数和程序初始化
1.1 程序从main函数开始吗
操作系统装载程序之后,首先运行的代码并不是main
的第一行,而是某些别的代码,这些代码负责准备好main
函数执行所需要的环境,并且负责调用main
函数,这时候你才可以在main
函数里放心大胆地写各种代码:申请内存、使用系统调用、触发异常、访问I/O。在main
返回之后,它会记录main
函数的返回值,调用atexit
注册的函数,然后结束进程。
运行这些代码的函数称为入口函数或入口点(Entry Point),视平台的不同而有不同的名字。程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分。一个典型的程序运行步骤大致如下:
- 操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。
- 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造,等等。
- 入口函数在完成初始化之后,调用
main
函数,正式开始执行程序主体部分。 main
函数执行完毕以后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。
1.2 入口函数如何实现
GLIBC入口函数
glibc是GNU发布的libc库,即c运行库,是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc的启动过程在不同的情况下差别很大,比如静态的glibc和动态的glibc的差别,glibc用于可执行文件和用于共享库的差别,这样的差别可以组合出4种情况,这里只选取最简单的静态glibc用于可执行文件的时候作为例子。
可以在这里下载glibc源码,源码的glibc/sysdeps
目录下有各个架构对入口函数的实现。glibc的程序入口为_start
(这个入口是由ld链接器默认的链接脚本所指定的,也可以通过相关参数设定自己的入口)。_start
由汇编实现,并且和平台相关,比如i386的_start
在glibc/sysdeps/i386/start.S
中,以下是选取其中的部分代码:
ENTRY (_start)/* %ebp清零,表明这是尊贵的最外层函数 */xorl %ebp, %ebp/* 调用_start之前装载器会把用户的参数和环境变量压入栈中,栈中结构存放如下:|bot|...| 0 |envn|...|env0| 0 |argn|...|arg0|argc;top|----------------环境变量-------------参数-----------stack growth----> */popl %esi /* %esi指向argc. */movl %esp, %ecx /* %ecx执行argv数组和evn数组的首地址.*//* 将调用__libc_start_main所需的参数压入栈 */andl $0xfffffff0, %esppushl %eaxpushl %esppushl %edx/* This used to be the addresses of .fini and .init. */pushl $0pushl $0pushl %ecx /* Push second argument: argv. */pushl %esi /* Push first argument: argc. */pushl $maincall __libc_start_mainEND (_start)
环境变量:是存在于系统中的一些公用数据,任何程序都可以访问。通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径、当前OS版本等。环境变量的格式为key=value
的字符串,C语言里可以使用getenv
这个函数来获取环境变量信息。
现在可以看到_start
只是对__libc_start_main
函数做了一个调用,__libc_start_main
函数实现在glibc/csu/libc-start.c
中,以下选取部分代码:
define LIBC_START_MAIN __libc_start_mainSTATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),int argc, char **argv,__typeof (main) init,void (*fini) (void),void (*rtld_fini) (void), void *stack_end){/* 让__environ指针指向环境变量数组的首地址 */char **ev = &argv[argc + 1]; __environ = ev;/* 一些关键函数调用的罗列 */__pthread_initialize_minimal ();__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);__libc_init_first (argc, argv, __environ);__cxa_atexit (call_fini, NULL, NULL);(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);__libc_start_call_main (main, argc, argv MAIN_AUXVEC_PARAM);
}
共有7个参数(现在的版本不止有7个,这里都是按照原书的内容介绍的):
- 第一个参数是main的函数指针,
argv
也包括了环境变量数组的指针 init
:main函数调用前的初始化工作fini
:main函数调用后收尾工作rtld_fini
:和动态加载有关的收尾工作,runtime loader。stack_end
:标明了栈低的地址,即最高栈地址
函数调用中,__cxa_atexit
函数用于将指定参数的函数(rtld_fini
和call_fini
)在main结束之后调用,末尾__libc_start_call_main
开始对main函数的调用,最终会通过_exit
调用系统调用exit()
,退出进程。
1.3 运行库与IO
一个程序的I/O指代了程序与外界的交互,包括文件、管道、网络、命令行、信号等。更广义地讲,I/O指代任何操作系统理解为”文件”的事务。许多操作系统,包括Linux和Windows,都将各种具有输入和输出概念的实体----包括设备、磁盘文件、命令行等----统称为文件,因此这里所说的文件是一个广义的概念。
对于一个任意类型的文件,操作系统会提供一组操作函数,这包括打开文件、读文件、写文件、移动文件指针等。C语言文件操作是通过一个FILE
结构的指针来进行的,fopen
返回一个FILE
指针,其他函数使用这个指针操作。
#include <stdio.h>int main()
{FILE *fp = NULL;fp = fopen("/tmp/test.txt", "w+");fprintf(fp, "This is testing for fprintf...\n");fputs("This is testing for fputs...\n", fp);fclose(fp);
}
在操作系统层面上,文件操作也有类似于FILE
的一个概念,在Linux里,这叫做文件描述符(File Descriptor),而在Windows里,叫做句柄(Handle)(以下在没有歧义的时候统称为句柄)。用户通过某个函数打开文件以获得句柄,此后用户操作文件皆通过该句柄进行。
设计这么一个句柄的原因在于句柄可以防止用户随意读写操作系统内核的文件对象。无论是Linux还是Windows,文件句柄总是和内核的文件对象相关联的,但如何关联细节用户并不可见。内核可以通过句柄来计算出内核里文件对象的地址,但此能力并不对用户开放。
举一个实际例子,Linux中,fd值为0、1、2时分别代表标准输入、标准输出和标准错误输出,所以在程序中打开文件得到的fd是从3开始增长。fd是什么呢?在内核中,每个进程都有一个私有的“打开文件表”,这个表是一个指针数组,每一个元素都指向一个内核的打开文件对象。而fd,就是这个表的下表。当用户打开一个文件时,内核会在内部生成一个打开文件对象,并在这个表里找到一个空项,让这一项指向生成的打开文件对象,并返回这一项的下标作为fd。由于这个表处于内核,用户无法访问到,因此用户就算有fd也不能直接得到打开文件对象的地址,只能通过系统提供的函数来操作。
FILE、fd、打开文件表和打开文件对象的关系如下图所示,内核指针p指向该进程的打开文件表,所以只要有fd,就可以通过p + fd
来得到打开文件表的某一项地址。stdin、stdout、stderr均是FILE的指针。
所以了解了IO机制之后,再来看I/O初始化的职责:首先I/O初始化函数需要在用户空间中建立stdin、stdout、stderr及其对应的FILE结构,使得程序进入main之后可以直接使用printf、scanf等函数。
二、C/C++运行库
2.1 C语言运行库
C语言运行库:任何一个C程序,它的背后都有一套庞大的代码来进行支撑,以使得该程序能够正常运行。这套代码至少包括入口函数,及其所依赖的函数所构成的函数集合。当然,它还理应包括各种标准库函数的实现。这样的一个代码集合称之为运行时库(Runtime Library)。而C语言的运行库,即被称为C运行库(CRT)。一个C语言运行库大致包含了如下功能:
- 启动与退出:包括入口函数及入口函数所依赖的其它函数等。
- 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现。
- I/O:I/O功能的封装和实现。
- 堆:堆的封装和实现。
- 语言实现:语言中一些特殊功能的实现。
- 调试:实现调试功能的代码。
2.2 C语言标准库
美国国家标准协会(American National Standards Institute, ANSI)在1983年成立了一个委员会,旨在对C语言进行标准化,此委员会所建立的C语言标准被称为ANSI C。
第一个完整的C语言标准建立于1989年,此版本的C语言标准称为C89。在C89标准中,包含了C语言基础函数库,由C89指定的C语言基础函数库就称为ANSI C标准运行库(简称标准库)。其后在1995年C语言标准委员会对C89标准进行了一次修订,在此次修订中,ANSI C标准库得到了第一次扩充,头文件iso646.h、wchar.h和wctype.h加入了标准库的大家庭。
在1999年,C99标准诞生,C语言标准库得到了进一步的扩充,头文件complex.h、fenv.h、inttypes.h、stdbool.h、stdint.h和tgmath.h进入标准库。
C11标准是C语言标准的第三版,前一个标准版本是C99标准。C11标准中又新增了5个头文件stdalign.h、stdatomic.h、stdnoreturn.h、threads.h、uchar.h。至此,C标准函数库共29个头文件。除了之前的14个头文件,剩下的15个头文件(C89标准)为:assert.h、ctype.h、errno.h、float.h、limits.h、locale.h、math.h、setjmp.h、signal.h、stdarg.h、stddef.h、stdio.h、stdlib.h、string.h、time.h。
关于每个头文件的介绍可以参考:C library或C库函数手册
2.3 glibc
运行库是平台相关的,因为它与操作系统结合得非常紧密。C语言的运行库从某种程度上来讲是C语言的程序和不同操作系统平台之间的抽象层,它将不同的操作系统API抽象成相同的库函数。比如我们可以在不同的操作系统平台下使用fread
来读取文件,而事实上fread
在不同的操作系统平台下的实现是不同的,但作为运行库的使用者我们不需要关心这一点。Linux和Windows平台下的两个主要C语言运行库分别为glibc(GNU C Library)和MSVCRT(Microsoft Visual C Run-time)。
值得注意的是,像线程操作这样的功能并不是标准的C语言运行库的一部分,但是glibc和MSVCRT都包含了线程操作的库函数。比如glibc有一个可选的pthread库中的pthread_create()
函数可以用来创建线程;而MSVCRT中可以使用_beginthread()
函数来创建线程。所以glibc和MSVCRT事实上是标准C语言运行库的超集,它们各自对C标准库进行了一些扩展。
glibc
glibc即GNU C Library,是GNU旗下的C标准库。最初由自由软件基金会FSF(Free Software Foundation)发起开发,目的是为GNU操作系统开发一个C标准库。
glibc的发布版本主要由两部分组成,一部分是头文件,比如stdio.h、stdlib.h等,它们往往位于/usr/include
;另外一部分则是库的二进制文件部分。二进制部分主要的就是C语言标准库,它有静态和动态两个版本,动态的标准库是/lib/xxx/libc.so.6
,静态标准库为/usr/lib/xxxlibc.a
。事实上glibc除了C标准库之外,还有几个辅助程序运行的运行库,这几个文件可以称得上是真正的”运行库”,它们就是/usr/lib/xxx/crt1.o
、/usr/lib/xxx/crti.o
、/usr/lib/xxx/crtn.o
,虽然它们都很小,但这几个文件都是程序运行的最关键的文件。
glibc 启动文件
crt1.o
里面包含的就是程序的入口函数_start
,由它负责调用__libc_start_main
初始化libc并且调用main函数进入真正的程序主体。
C++必须要在main()
函数之前执行全局/静态对象构造和必须要在main()
函数之后执行全局/静态对象析构,为了满足此类需求,运行库在每个目标文件(ELF)中引入两个初始化相关的段”.init
”和”.finit
”,运行库会保证所有位于这两个段的代码先于/后于main()
函数执行。链接器在进行链接时,会把所有输入目标文件中的”.init
”和”.finit
”按照顺序收集起来,然后将他们合并成输出文件的”.init
”和”.finit
”。但是这两个输出段中所包含的指令,还需要一些辅助的代码帮助他们启动,于是引入了两个目标文件crti.o
和crtn.o
。
crti.o
和crtn.o
两个目标文件中包含的代码实际上是_init()
函数和_finit()
函数的开始和结尾部分,当这两个文件和其它目标文件按照顺序链接起来以后,刚好形成两个完整的函数_init()
和_finit()
,所以最终输出文件中的”.init
”和”.finit
”两个段实际上分别包含的是_init()
和_finit()
函数。可以用objdump查看这两个文件的反汇编代码:
$ objdump -dr /usr/lib/mips64el-linux-gnuabi64/crti.o /usr/lib/mips64el-linux-gnuabi64/crti.o: 文件格式 elf64-tradlittlemipsDisassembly of section .init:0000000000000000 <_init>:0: 67bdfff0 daddiu sp,sp,-164: ffbc0000 sd gp,0(sp)8: 3c1c0000 lui gp,0x08: R_MIPS_GPREL16 _init8: R_MIPS_SUB *ABS*8: R_MIPS_HI16 *ABS*c: 0399e02d daddu gp,gp,t910: ffbf0008 sd ra,8(sp)14: 679c0000 daddiu gp,gp,014: R_MIPS_GPREL16 _init14: R_MIPS_SUB *ABS*14: R_MIPS_LO16 *ABS*18: df820000 ld v0,0(gp)18: R_MIPS_GOT_DISP __gmon_start__18: R_MIPS_NONE *ABS*18: R_MIPS_NONE *ABS*1c: 10400004 beqz v0,30 <_init+0x30>20: 00000000 nop24: df990000 ld t9,0(gp)24: R_MIPS_CALL16 __gmon_start__24: R_MIPS_NONE *ABS*24: R_MIPS_NONE *ABS*28: 0320f809 jalr t928: R_MIPS_JALR __gmon_start__28: R_MIPS_NONE *ABS*28: R_MIPS_NONE *ABS*2c: 00000000 nopDisassembly of section .fini:0000000000000000 <_fini>:0: 67bdfff0 daddiu sp,sp,-164: ffbc0000 sd gp,0(sp)8: 3c1c0000 lui gp,0x08: R_MIPS_GPREL16 _fini8: R_MIPS_SUB *ABS*8: R_MIPS_HI16 *ABS*c: 0399e02d daddu gp,gp,t910: ffbf0008 sd ra,8(sp)14: 679c0000 daddiu gp,gp,014: R_MIPS_GPREL16 _fini14: R_MIPS_SUB *ABS*14: R_MIPS_LO16 *ABS*
$ objdump -dr /usr/lib/mips64el-linux-gnuabi64/crtn.o /usr/lib/mips64el-linux-gnuabi64/crtn.o: 文件格式 elf64-tradlittlemipsDisassembly of section .init:0000000000000000 <.init>:0: dfbf0008 ld ra,8(sp)4: dfbc0000 ld gp,0(sp)8: 03e00008 jr rac: 67bd0010 daddiu sp,sp,16Disassembly of section .fini:0000000000000000 <.fini>:0: dfbf0008 ld ra,8(sp)4: dfbc0000 ld gp,0(sp)8: 03e00008 jr rac: 67bd0010 daddiu sp,sp,16
于是在最终链接完成之后,输出的目标文件中的”.init”
段只包含了一个函数_init()
,这个函数的开始部分来自于crti.o
的”.init
”段,结束部分来自于crtn.o
的”.init
”段。为了保证最终输出文件中的”.init
”和”.finit
”的正确性,我们必须保证在链接时,crti.o
必须在用户目标文件和系统库之前,而crtn.o
必须在用户目标文件和系统库之后。链接器的输入文件顺序一般是:
ld crt1.o crti.o [user_objects] [system_libraries] crtn.o
由于crt1.o
不包含”.init
”段和”.finit
”段,所以不会影响最终生成”.init
”和”.finit
”段时的顺序。
在默认情况下,ld链接器会将libc、crt1.o
等这些CRT和启动文件与程序的模块链接起来,但是有些时候,我们可能不需要这些文件,或者希望使用自己的libc和crt1.o
等启动文件,以替代系统默认的文件,这种情况在嵌入式系统或操作系统内核编译的时候很常见。GCC提供了两个参数”-nostartfile
”和”-nostdlib
”,分别用来取消默认的启动文件和C语言运行库。
其余部分感觉暂时用不到,以后如果用到在补充
链接装载与库:第十一章——运行库相关推荐
- 计算机的库的分类,计算机运行库的分类和简介
我们在使用软件的时候,经常会碰到"该软件需要 VC++?支持""应用程序配置不正确,未能启动成功""不是有效的Win32程序"等错误提示,而 ...
- mfc140dll 丢失 微软常用运行库_集成最新运行库、一键安装、一键到位,运行库操作简单!...
背景有很多童鞋在安装诸如PS.vc++等软件总会提示缺少关键组件或者运行框架,也有在后台问我的,所以今天就给大家分享一下一些电脑安装软软件或者游戏常用的运行库,号主找了一些并且写了批处理直接运行就可以 ...
- mfc140dll 丢失 微软常用运行库_微软常用运行库合集 2020.9月(32amp;64位)
每日一谈 电脑在刚买来的时候,我们常常会思考一个问题,那就是微软的运行库有必要安装吗?微软运行库有什么用?其实在这里我想说,微软运行库主要是运行计算机软件的,例如一些测绘的职业人员,需要用到Auto ...
- linux c 11 运行库,11.1.3 运行库与I/O
11.1.3 运行库与I/O 在了解了glibc和MSVC的入口函数的基本思路之后,让我们来深入了解各个初始化部分的具体实现.但在具体了解初始化之前,我们要先了解一个重要的概念:I/O. IO(或I ...
- mcinabox运行库下载安装_mcinabox运行库
mcinabox运行库是一款超棒的我的世界启动工具.轻松快速的打开我的世界游戏,完美的进行游戏相关性设置,多种mod快速导入,各种资源完美管理,非常好用,可以自动检测libraries文件和Asset ...
- windows c语言运行库,Microsoft Visual C++ 运行库合集下载
使用方法: vc2010运行库安装失败解决方法 一般来说,在控制面板中正确卸载后,是可以正常重新安装的.如果 卸载.重启.安装微软官方版本,这样做依然有提示,那么你的电脑底层已经不稳定了,造成这类问题 ...
- mcinabox运行库下载_【mcinabox运行库下载】mcinabox运行库 v20200328 官方版-开心电玩...
软件介绍 mcinabox运行库是一款提供给我的世界玩家们使用的Java版启动器,我们如果想要在自己的手机上开启<我的世界>的话,就必须要先安装mcinabox运行库才可以.有了这款软件, ...
- C标准库和glibc(C运行库)的关系
C 标准主要由两部分组成,一部分描述C的语法,另一部分描述C标准库.C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数.变量.类型声明和宏定义.要在一个平台上支持C语言,不仅要实现C编译器, ...
- 壁纸引擎java运行库_Microsoft Windows Desktop Runtime v5.0.0 桌面程序运行库(含常规运行库)...
2020.11.11 微软.NET 5.0 正式版发布! 本次版本更新非常重视 .NET 的统一性,微软希望 .NET Framework 开发者能够迁移他们的代码和应用到 .NET 5.0 上,为明 ...
- 【读书笔记】【程序员的自我修养 -- 链接、装载与库(三)】函数调用与栈(this指针、返回值传递临时对象构建栈、运行库与多线程、_main函数、系统调用与中断向量表、Win32、可变参数、大小端
文章目录 前言 介绍 内存 内存布局 栈与调用惯例 堆与内存管理 运行库 入口函数和程序初始化 C/C++运行库 运行库与多线程 C++全局构造与析构 fread 实现 系统调用与API 系统调用介绍 ...
最新文章
- 【实习】京东搜索相关性算法部门
- hibernate mysql 映射_使用hibernate建立mysql连接以及生成映射类和配置文件*.cfg.xml
- 职场社交方向私密研究(脉脉、linkedin、会会…)| 北京活动
- 提升Web用户体验的71个设计要点
- 微服务技术栈及分享计划
- python——time模块实现指定时间触发器
- python 验证码图片 模拟登录_Python 模拟生成动态产生验证码图片的方法
- iOS 日志管理异常捕获组件LFLogManager
- 【免费毕设】ASP.NET猜数游戏的设计与开发(源代码+lunwen)
- Ubuntu12.04中如何让命令行路径变短
- 数字信号处理(第四版)pdf
- Linux 终端命令的末尾加上一个 符号的作用
- 算法:工作窃取算法(work-stealing)。
- C++描述 1113. 红与黑
- 对于最小二乘法的解释
- qq无法启动此程序计算机中丢失dll,Win7系统打开QQ提示丢失DLL文件怎么办
- 基于VS2017的C++ SuperLU混合编程
- 实数截断式保留两位小数
- 微信公众平台开发——新增素材
- word中插入图表改变数据系列产生在行或列
热门文章
- Topaz DeNoise AI 3.7 人工智能降噪
- 艾宾浩斯曲线在线 PDF 文件生成 在线背单词 背单词计划表 高考-四级-六级-SAT-托福-雅思-GRE-17 天搞定 GRE 单词, 背单词神器-动态生成时间表! 利用艾宾浩斯记忆曲线自动生成背单
- XF660R型号良田高拍仪接口开发,通过图片文件的二进制数据进行图片上传
- HTML页面跳转的方法
- Java 对象转化为Map
- 高等数学学习笔记——第九十五讲——函数的幂级数展开
- Hadoop集群安装配置教程_Hadoop2.6.0_UbuntuCentOS(林子雨教授,超级详细)
- “焊”卫锂电 | 昂视锂电池密封钉视觉检测应用详解
- arcgis怎么压缩tif文件_PDF文件怎么压缩?这个方法一看就会!
- 词根词缀的实践应用 - 词根词缀词典墨墨详细使用