首页

源码搜藏网

首页 > 开发教程 > 软件工程 >

读《程序员的自我修养 —— 库与运行库》乱摘

创建时间:2016-06-12 17:37  

2016.06.07 -
读《程序员的自我修养 —— 链接、装载与库》“库与运行库”的个人理解笔记。

06.07
一个程序典型的运行环境:程序(内存),运行库,API,内核。

1 内存

1.1 程序的内存布局

读《程序员的自我修养 —— 库与运行库》乱摘
Linux进程虚拟地址空间分布

1.2 栈与调用惯例

(1) 栈

。栈是一个特殊的容器(抽象的描述)。将数据放入栈中叫作入栈;将数据从栈中取出来称为出栈。出栈和入栈需要遵循的规则:先入栈的数据后出栈。

在程序运行过程中,栈保存了一个函数调用所需要的维护信息(堆栈帧)。

06.08
一般,可以用esp寄存器指向栈顶,用ebp指向栈底。
在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也指向了当前函数的活动记录的顶部。而相对的,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。一个常见的活动记录如下图所示:
读《程序员的自我修养 —— 库与运行库》乱摘

esp始终指向栈顶(随着函数的执行,esp值会发生变化)。ebp固定在图中所示的位置,不随函数的执行而发生变化。

例 – 调用一个函数的活动记录。
…(反汇编)

(2) 调用惯例

即对“函数参数的传递顺序和方式”、“栈的维护方式”、“名字修饰的策略”等的规定。

(3) 函数返回值传递

对于4个字节来说,函数将返回值存储在eax中,返回后函数的调用方再读取eax。对于5 ~ 8字节对象的情况,几乎所有的调用惯例都是采用eax和edx联合返回的方式进行的。对于超过8字节的情况,这些内容会被存储到内存中,然后将该内存的地址存到eax中,返回后函数的调用方再读取eax。

例 – 4字节,5-8字节,多字节返回值的传递过程。

1.3 堆与内存管理

在进程的虚拟地址空间中,除了可执行文件、共享库和栈之外,剩余的未分配的空间都可以被用来作为堆空间。
Linux下的进程管理堆提供了两种堆空间分配的方式:brk()和mmap()系统调用。

2 运行库

2.1 入口函数和程序初始化

整个过程
操作系统加载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码复杂准备好main函数执行所需要的环境,并且负责调用main函数。在main返回之后,它会记录main函数的返回值,调用(atexit)注册的函数,然后结束进程。运行(包含)这些代码的函数称为入口函数或入口点,视平台的不同而有不同的名字。程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分。一个典型的程序运行步骤大致如下:

06.09
glibc启动代码(静态glibc用于可执行文件的例子)。
glibc源代码的子目录libc/csu中有关于程序启动的代码。
glibc的程序入口为ld链接器默认指定的(可修改)_start。_start由汇编实现,跟平台相关(可以用函数的活动记录知识查看_start汇编代码)。

环境变量。环境变量是存在于系统中的一些公用数据,任何程序都可以访问。通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径,当前OS版本等。环境变量的格式为key=value的字符串,C语言里可以使用getenv这个函数来获取环境变量信息。

例 – 查看_start函数(反)汇编代码,了解入口函数调用程序入口的过程。
[ _start –>__libc_start_main(…–>调用main函数 –> exit) ]

运行库与I/O
在了解glibc入口函数(启动代码)的基本思路后,是了解各个部分的具体实现的时候了。

程序角度的I/O。一个程序的I/O指代了程序程序与外界的交互,包括文件、管道、网络、命令行、信号等。

句柄。Linux下的文件描述符在Windows下叫作句柄。[By the way]

I/O初始化。首先I/O初始化函数需要在用户空间中建立stdin、stdout、stderr及其对应的FILE结构,使得程序进入main之后可以直接用printf、scanf等函数。

2.2 C语言运行库

C运行库(CRT)。包含入口函数、所依赖的函数所构成的函数集合及其包括各种标准库函数的实现。由它支持一个C程序的正常运行。一个C语言库大致包含了如下功能:

C标准库
C语言标准库占据了C运行库的主要地位。
例 – 变长参数。

例 – 非局部跳转(setjmp)。

glibc。glibc主要由两部分组成,一部分是头文件,比如stdio、stdlib等,它们往往位于/usr/include;另外一部分则是库的二进制文件。二进制部分主要的就是C语言标准库,它有静态和动态两个版本。(glibc除了C标准库外,还有几个辅助程序运行的运行库,这几个文件可以称得上真正的“运行库”。它们就是/usr/lib/crtl.o、/usr/lib/crti.o。)[By the way]

glibc启动文件。crtl.o里面包含的就是程序的入口函数_start,由它负责调用__libc_start_main初始化libc并且调用main函数进入真正的程序主体。其包含了基本的启动、退出代码。

例 – 查看启动与退出的汇编代码。(.init和.finit)

2.3运行库与多线程

线程访问权限。线程的访问能力非常自由,它可以访问线程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,然而这是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括:

多线程运行库。对于C/C++标准库来说,线程相关的部分是不属于标准库的内容的,它跟网络、图形图像等一样,属于标准库之外的系统相关库。(多线程操作接口?运行库本身支持多线程环境 —— 如C标准库中的errno:大多数错误代码是在函数返回之前赋值在名为errno的全局变量里的。多线程并发的时候,有可能A线程的errno的值在获取之前就被B线程给覆盖掉,从而获得错误的出错信息)

2.4 fread实现

例 – fread实现。

3 系统调用与API

3.1 系统调用介绍

由于系统有限的资源有可能被多个不同的应用程序同时访问,因此,如果不加以保护,那么各个应用程序难免产生冲突。所以现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。这些系统资源包括文件、网络、IO、各种设备等。

为了让应用程序有能力访问系统资源,也为了让程序借助操作系统做一些必须由操作系统支持的行为,每个操作系统都提供一套接口,以供应用程序使用。这些接口往往通过中断来实现(Linux使用0x80号中断作为该套接口的入口,Windows采用0x2E)。

Linux系统调用。在x86下,系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号,比如EAX=1表示退出进程(exit);EAX=2表示创建进程(fork);EAX=3表示读取文件或IO(read);EAX=4表示写文件或IO(write)等,每个系统调用都对应于内核资源代码中的一个函数,它们都是以“sys_”开头的,比如exit调用对应内核中sys_exit函数。当系统调用返回时,EAX有座位调用结果的返回值。[见函数返回值传递]。这些系统调用都可以在程序里直接使用,它的C语言形式被定义在“usr/include/unistd.h”中[比如glibc的fopen、fread、fclose打开读取和关闭文件,而直接使用open()、read()和close()来实现文件的读取,使用write向屏幕输出字符串(标准输出的文件句柄为0)]。

3.2 系统调用原理

中断触发(int 80h) –> 堆栈切换 –> 中断处理程序。

特权级与中断。现代的CPU常常可以在多种截然不同的特权级别下执行指令,在现代操作系统中,通常也据此由两种特权级别,分别为用户模式内核模式,也被称为用户态和内核态。由于有多种特权模式的存在,操作系统就可以让不同的代码运行在不同的模式上,以限制它们的权力,提高稳定性和安全性。普通应用程序运行在用户态的模式下,诸多操作将受到限制,这些操作包括访问硬件设备、开关中断、改变特权模式等。操作系统一般是通过中断来从用户态切换到内核态。

系统调用原理见《30天自制操作系统》更详细(有源码样例)。

堆栈切换。在实际执行中断向量表中的第0x80号元素所对应的函数之前,CPU首先还要进行栈切换。在Linux中,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。在应用程序调用0x80中断时,程序的执行流程从用户态切换到内核态,这时程序的当前栈必须也相应地从用户栈切换到内核栈。从中断处理函数中返回时,程序的当前栈(esp的值所在的栈空间,ss值为当前栈所在的页)还要从内核栈切换回用户栈。

将当前栈由用户栈切换到内核栈的实际行为就是:

反过来,将当前栈由内核栈切换为用户栈的实际行为则是:

当0x80号中断发生的时候,CPU除了切入内核态之外,还会自动完成下列事:

而当内核从系统调用中返回的时候,需要调用iret指令来回到用户态,iret指令则会从内核栈里弹出寄存器SS、ESP、EFLAGS、CS、EIP的值,使得栈恢复到用户态的状态。
读《程序员的自我修养 —— 库与运行库》乱摘

中断处理程序
读《程序员的自我修养 —— 库与运行库》乱摘

Linux新型系统调用机制
由于int指令的系统调用在奔腾4代处理器上性能不佳,Linux在2.5版本起开始支持一种新型的系统调用机制。这种新机制使用Intel在奔腾2代处理器就开始支持的一种专门针对系统调用的指令 —— sysenter和sysexit。

4 运行库实现

06.11

4.1 迷你C运行库

例 – 实现一个迷你CRT,具备入口函数、初始化、堆管理、基本IO。

[1] 入口函数
程序运行最初入口点是由运行库为其提供的入口函数。它主要负责三部分工作:准备好程序运行环境及初始化运行库,调用main函数执行程序主体,清理程序运行后的各种资源。运行库为所有程序提供的入口函数应该相同,在链接程序时需要制定该入口函数名。
入口函数框架

main参数

初始化部分

结束部分

[2] 堆的实现

[3] IO与文件操作

[4] 字符串相关操作
….
[5] 格式化字符串

4.2 使用迷你C运行库

一般一个CRT提供给最终用户时往往有两部分,一部分是CRT的库文件部分,用于与用户程序进行链接,如Glibc提供了两个两个版本的库文件:静态Glibc库libc.a和动态Glibc库libc.so。CRT另外一个部分就是它的头文件,包含了使用该CRT所需要的所有常数定义、宏定义及函数声明。

[1] 建立包含所有相关常数定义、宏定义以及迷你CRT所实现的函数声明的头文件minicrt.h。
minicrt.h

[2] 编译得到(静态)库文件

[2016.06.12 - 10:46]

0
0
   
上一篇:libuv学习笔记(5)
下一篇:负载均衡的那些算法们

相关内容

热门推荐