• Computer
  • 程序员的自我修养

    编译链接和库 –

    Tag: computer system

    compile link and lib</img>

    作者:俞甲子/石凡/潘爱民

    相关command记录:

    1. readelf -s *.o :产看.o文件符号表

    2. ar -t libc.a :查看库中所包含的目标文件

    3. ar -x libc.a : 解压库

    4. gcc -static –verbose -fnobuiltin *.c : -verbose 将编译连接过程所有的过程打印出来,

    5. ar -t xx.a 查看库文件信息

    Computer System

    • 南桥(ISA):low speed bus

    • 北桥(PCI):high speed bus

    多道系统->分时系统->多任务系统 的演进

    线程调度与优先级

    starvation(饿死):按照优先级进行调度,低优先级可能长时间不能得到CPU 资源,出现线程饿死状况

    linux下线程创建:

    • fork

      fork 采用写时复制方式修改内存空间,在非写内存时,与父进程共享一份内存空间。

    • exec

      新可执行映像覆盖当前的可执行映像

    • clone

      产生一个新任务,创建新的内存空间

    函数重入:在函数未执行完成的情况下,被外部因素打断又一次进入该函数执行,外部因素出现的情况是:

    • 多个线程执行该函数
    • 函数被自身调用

    可重入条件:

    • 不使用和返回任何静态或全局的非const变量
    • 仅依赖于调用方提供的参数
    • 不依赖任何单个资源的锁(mutex等)
    • 不调用任何不可重入的函数

    线程安全的考虑:

    • 编译器优化将变量缓存至寄存器中==>Valotile 关键字
    • 编译器优化调整指令执行顺序==>设置barrier,强迫编译器不做调整

    内存空间

    linux 内存空间布局:

    • 可执行映像文件
    • 保留区

    引入内存映射的考虑:

    • 地址空间隔离
    • 内存使用效率低:程序空间从内存到硬盘的频繁换入换出,造成的效率低下
    • 程序地址不确定

    • 返回地址
    • 临时变量
    • 上下文

    一个function foo 分析:

    int foo()
    {
    	return 123;
    }
    

    反汇编: push ebp mov ebp,esp //1. store ebp,and new ebp point to current esp

    sub esp,0C0h    //2. prepare new stack space
    
    push ebx
    push esi
    push edi		//3. store: ebx,esi,edi
    
    lea edi,[ebp-0c0h]
    mov ecx,30h
    mov eax,0ccccccccch
    rep	stos dword ptr es[edi]	//4. add debug information
    
    mov eax,7Bh		//5. return value
    
    pop edi
    pop esi
    pop ebx			//6.restore edi,esi,ebx
    
    mov esp,ebp
    pop ebp			//7. restore esp,ebp
    
    ret				//8. ret return
    

    note:单个字节的返回值存储在eax寄存器中。

    堆空间频繁采用系统调用–低效

    系统调用申请大块内存,由程序进行管理分配,提高效率–高效

    linux 进程堆管理

    两种堆空间分配方式:

    • int brk(void *end_data_segment)

      设置进程结束段的地址

    • void mmap(void *start,size_t length,int prot,int flags, int fd, off_t offset) 参数含义依次为:起始地址,空间长度,空间权限(prot flags),指定文件描述符和文件偏移(fd,offset)

      mmap可以向系统申请一段虚拟地址空间,这段空间可映射到某个具体文件,当无具体文件映射时,这段地址空间为匿名空间,匿名空间可以作为堆来使用。

      see detail in: linux manpage

    glibc 中的malloc函数:

    mem size<128KB,在现有的堆空间中进行分配

    mem size>128KB,调用mmap函数申请匿名空间

    Linux kernel 内存分配状况:2.6

    windows 进程堆管理

    to be discussed

    堆分配算法
    空闲链表

    当请求一块内存a时,将一块大于a的内存分为两块,将该空闲块的指针更新为新的剩余空闲块的指针。如果为零,则直接删除原来的空闲块结点。

    位图

    High:11 00 00 10 10 10 11 00 00 00 00 00 00 00 10 :Low

    eg: HeapSize=1MB

    BlockSize=128KB

    BlockNumber=HeapSize/BlockSize=8K

    8k/(32/2)=512 int表示

    核心思想:

    • 将堆分成若干个块
    • 每个块标记成Head, Block, Free,可用两个bit区分标记
    • 将满足申请size需求的整数个块分配给用户,首个块为Head,其余为Block

    优缺点:

    • 速度快:直接访问数组,空闲空间容易命中
    • 稳定性好:可对数组做备份,防止用户修改,不造成整个heap无法使用
    • 块冗余管理信息少,易于管理
    • _易产生内存碎片_
    • _为了减少内存碎片,缩小块的大小,这时会造成位图数组很大,削弱速度快的优势(ps:可采用多级位图)_
    对象池

    核心思想: 假定每次申请的heap size一定,简化对象池中heap分配的过程

    ??对象池之外,是否需要额外的管理,保证每次申请的heap size一定

    运行库

    一个典型的程序运行大致步骤:

    1. 操作系统创建进程,控制权交给程序入口(运行库中的某个函数)
    2. 入口函数对运行库和程序运行环境进行初始化(堆,IO,线程,全局变量等)
    3. 调用main
    4. main结束后,返回入口函数,执行清理工作(全局变量析构,堆销毁,关闭IO等)

    note:在嵌入式中,第一步操作系统的工作由Arm或51的内核完成,同时不创建进程。

    glibc入口函数

    c运行库(CRT)

    c运行库的功能:

    1. 启动与退出(入口函数及其依赖函数)
    2. 标准函数
    3. I/O:I/O功能封装
    4. 堆:堆的封装和实现
    5. 语言实现:语言中一些特殊功能实现
    6. 调试:实现调试功能

    。。。

    系统调用与API

    对于Windows来说,系统调用即window的API(API间接调用系统接口)

    Linux系统调用

    x86架构下,由中断号0x80 中断完成

    Linux下的系统调用接口:

    • exit
    • fork
    • read
    • write

    可以直接绕过运行库中的对应接口而直接采用系统调用:

    如使用open(),read(),write()等替换fopen()…

    __系统调用的缺陷:__增加移植的困难,削弱兼容性

    系统调用原理

    计算机系统一般分为两种模式:内核模式(特权模式),用户模式 两种模式间的切换:

    i386下系统调用中断号:

    • windows int 0x2e
    • linux int 0x80

    多个系统调用通过同一中断号产生中断,由__系统调用号进行区分__

    linux在不同模式下进行切换的同时,要对栈也进行同步切换,即从用户模式到内核模式时,要保存当前用户模式的栈参数。

    • 切换到当前进程的内核栈(每一个进程都有自己的内核栈)
    • 保存用户模式的寄存器至内核栈中

    编译

    编译过程在整个生成可执行文件过程中的位置: .c文件->(预处理)-> .i文件 ->(编译)-> .s文件->(assemble 汇编)->.o ->(linker 链接)->.out

    编译过程我的理解即将文本文件中的code翻译为计算机所理解的symbol的过程。 其中包括:

    • 词法分析:扫描过程,lex程序完成,code被解析成分离的token
    • 语法分析:Grammar Parser,生成语法树
    • 语义分析:Semantic Analyzer,
    • 中间代码生成:简化一些在编译期间即可确定的过程
    • 目标代码生成与优化

    链接

    对链接的理解需要统一静态链接与动态链接,两部分进行对比,在本书中,这两部分分为前后的两大块,相隔时间一长,比较起来不方便,容易看了后边忘了前边的实现细节,这边,把静态链接与动态链接(共享库)部分整合到一起,重新梳理下链接的整个过程以及技术演化的历史。

    链接过程主要包括:重定位、地址和空间分配、符号决议(symbol resolution) 编译过程会为交互符号提供重定位入口(relocation entry)

    首先是地址和空间分配过程,静态链接过程会将不同.o文件的相同段组合在一起,相同的段共用同一个段空间,

    随后会进行符号解析和重定位过程: 在编译阶段,外部symbol的地址是不确定的,以伪地址代替,在链接阶段,要对外部symbol的地址进行重定位。

    在重定位过程中,处理不同文件的同名弱符号(多个同名强符号会报错),链接器会以common块的处理方式处理这种情况,即以同名symbol中类型占据空间较大的为标准, 弱符号的定义:未初始化的全局变量

    C++中链接问题

    模板问题(模板膨胀):通过将模板单独放入一个段中,相同的模板最终链接到同一个段中。

    函数级别链接:将未使用的函数从最终生成的文件中剔除。

    构造和析构 elf文件提供了两个独立的段执行main函数前的初始化工作(全局对象构造函数在main函数进行调用,析构函数在main函数之后被执行) .init 段 .fint 段

    链接不同编译器的目标文件

    保证ABI相同(application binary interface):

    1. 采用同样的目标文件格式;
    2. 同样的符号修饰标注;
    3. 变量的内存分配方式相同;
    4. 函数的调用方式相同。
    • c++的结构复杂造成其生成的二进制代码在不同的编译器下会产生较c更大的差异,移植起来更加复杂和艰难。

    动态链接,共享库

    静态链接的缺点:

    • 相同模块重复占用内存空间
    • 相同模块重复占用磁盘空间:image文件