zoukankan      html  css  js  c++  java
  • ###《程序员的自我修养》

    程序员的自我修养。

    #@author:       gr
    #@date:         2014-03-01
    #@email:        forgerui@gmail.co
    

    第一章、温故而知新

    1.1. 计算机硬件

    使用南桥处理低速设备:鼠标、键盘、磁盘
    使用北桥处理高速设备:CPU、内存、PCI总线

    1.2. 计算机软件架构

    分层解决,提供接口:
    Application => Runtime Library => Operating System Kernel => Hardware

    操作系统除了提供抽象的接口,另一个功能是管理硬件资源:CPU、存储器、I/O设备。

    1.3. 内存分配

    存在的问题:

    1. 地址空间不隔离,程序之间不影响
    2. 内存使用效率低,大量数据换入换出效率低
    3. 程序运行的地址不确定,每次载入程序的内存地址可能发生变化

    虚拟地址通过增加中间层,通过映射,将这个虚拟地址转换成实际的物理地址。

    分段:
    将程序所需的内存空间大小的虚拟空间映射到物理空间。可以解决问题一、三。分段对内存区域的映射还是按照程序为单位,如果内存不足,整个程序将被换出。

    分页:
    分页大小默认是4KB。换入换出以做为单位,大大降低换入换出的数据。

    虚拟存储通过一个叫MMU(Memory Management Unit)的部件来进行页映射。程序中的地址是虚拟地址,经过MMU转换为物理地址。一般MMU集成在CPU内部。

    1.4 线程

    Windows对进程和线程区分得很清楚。Linux内核中并不存在真正意义上的线程概念。Linux的执行实体都称为任务,每个任务类型于一个单线程的进程,但Linux不同的任务之间可以选择共享内存空间,所以实际意义上,共享了同一个内存空间的多个任务构成了一个进程。可以使用fork, exec, clone等创建新的任务。

    四种进程或线程同步互斥的控制方法
    1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
    2、互斥量:为协调共同对一个共享资源的单独访问而设计的。
    3、信号量:为控制一个具有有限数量用户资源而设计。
    4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

    第二章、编译和链接

    2.1. 编译过程

    预编译、编译、汇编、链接

    2.2 编译器的工作

    扫描 => 词法分析 => 语法分析 => 语义分析 => 中间语言生成 => 目标代码生成与优化

    2.3 链接器的工作

    地址和空间分配, 符号决议(Symbol Resolution),重定位

    第三章、目标文件是有什么

    3.1. objdump

    1. objdump -h : 输出各个段表
    2. objdump -d : 反汇编程序
    3. objdump -s : 以十六进制打印
    4. objdump -x : 输出全的信息,包括段表,符号表,程序头等

    3.2. readelf

    1. readelf -h : 显示ELF文件头
    2. readelf -S : 显示程序段表
    3. readelf -s : 显示符号表
    4. readelf -r : 重定位表
    5. readelf -l : 查看ELF文件的程序头,"Segment"

    objdump vs readelf:

    objdump可以进行反汇编。

    3.3. rodata段

    const 变量会放到.rodata段。

    3.4. bss data

    C中未初始化全局变量放在bss段中,且并没有分配空间,只是记录大小。初始化的变量放在data段中。

    bss:

    #include <stdlib.h>
    #include <stdio.h>
    
    int a[10000];
    
    int main(){
        int c;
    
        getchar();
        return 0;
    }
    
    
    readelf -S bss
    

    Alt text

    readelf -s bss
    

    Alt text
    Alt text

    data:

    #include <stdlib.h>
    
    int a[10000]={1};
    
    int main(){
        getchar();
        return 0;
    }
    
    readelf -S data
    

    Alt text

    readelf -s data
    

    Alt text
    Alt text

    3.5. bss段(C/C++)

    C++的所有全局对象都被以“初始化过的数据”来对待,都是作为强符号来使用的,而C中的未初始化的全局变量(包括初始为0)则只记录到BSS段中,不占用空间,是弱符号,所以可以在两个文件中声明相同的全局变量。

    3.6. ELF结构

    ELF Header -> .text -> .data -> .bss -> other sections -> Section header table -> String Tables, Symbol Tables

    文件头位于文件的最前部,它包括了描述整个文件的基本属性,比如ELF文件版本、目标机器型号,程序入口地址等。紧接着是各个段,之后是描述段属性的段表,比如段的大小,偏移,段名,读写权限。

    ELF文件头中定义了ELF魔数、文件机器字节长度,数据存储方式、版本、运行平台、ABI版本、入口地址、段表的位置、段的数量、目标平台等。

    3.7. Magic(魔数)

    从文件头中得到如下信息:

    Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
    Class:                             ELF64
    Data:                              2's complement, little endian
    Version:                           1 (current)
    OS/ABI:                            UNIX - System V
    ABI Version:                       0
    

    前面的16个字节,正好对应“Elf32_Ehdr”的e_indent这个成员,这16个字节被ELF标准规定用来标识ELF文件的平台属性。

    1. 7f 45 4c 46分别对应DEL控制符, E, L, F。这4个字节被称为ELF文件的魔数。a.out格式最开始的两个字节是0x01, 0x07,PE文件是0x4d, 0x5a。
    2. 02表示操作系统位数,0表示无效文件,1表示32位文件,2表示64位文件,这里表示是64位文件。
    3. 01表示是大端还是小端,0无效格式,1小端格式,2大端格式。
    4. 01表示ELF文件的主版本号,一般是1,因为ELF标准自1.2版本后就一直没有更新:-)
    5. 后面的9位还没有定义,在一些平台,会使用这9个字节扩展。

    3.8. 段表

    每一个段的信息放到一个“Elf32_Shdr”类型的结构里,多个段组成一个“Elf32_Shdr”的数组。

    3.9. 重定位表

    链接器在处理目标文件,须要对目标文件中某些部位进行重定位。有一个".rel.text"段,它的类型为"SHT_REL",它就是一个重定位表。它是针对".text"段的重定位表。

    3.10. 字符串表(String Table)

    ELF文件用到了许多字符串,比如段名、变量名等。把字符串集中起来放到一个表,用字符串在表中的偏移来引用字符串。
    字符串表一般以段的形式保存,常见的段名为“。字符串表".strtab”存储一般的字符串,段表字符串表“.shstrtab”存储段表中用的字符串,比如段名。

    3.11 符号表

    在链接中,将函数和变量统称为符号,函数名或变量名就是符号名。
    符号表往往也存储在文件中的一个段,叫".symtab"。其中,每个Elf32_Sym结构对应一个符号,它也是一个数组。

    typedef struct{
    	Elf32_Word st_name;			//符号名,利用字符串表的下标
    	Elf32_Addr st_value;		//符号相应的值,地址
    	Elf32_Word st_size;			//符号大小,如double占8个字节,类类型也有大小
    	unsigned char st_info;		//符号类型和绑定信息
    	unsigned char st_other;		//目前为0, 没用
    	Elf32_Half st_shndx;		//符号所在的段
    }Elf32_Sym;
    

    Problems:

    1. 全局静态变量肯定会保存在符号表中,但是对于局部静态变量或者一个类中的静态成员变量,会保存在符号表中吗?为什么呢?

      符号表的作用主要在于用来进行链接,局部静态变量或者一个类中的静态成员变量如果不进行debug的话, 是没有必要保存在符号表中的。

    2. 如果全局静态变量是个比较复杂的class,那么符号表在编译时就能确定class的大小吗?如果不能确定,怎么能够把这个class放到符号表中呢?

      如果在程序中能使用某一类型(包括类)定义一个此类型的变量,那它一定是一个完整类型,即类型的大小已知。

    3.12 符号修饰(mangling)

    C语言会在相对应的符号名前加上""。现在LInux下的GCC已经去掉了"",而Windows下还保存这种习惯。
    C++语言更强大而复杂,为了避免冲突,需要进行“Name Mangling”。Visual C++的名称修饰规则没有对外公开。GCC的C++修饰方法如下:所有符号都以"_Z"开头,对于嵌套的名字,后面紧跟N,然后是各个名称空间和类的名字,每个名字前是字符串的长度,再以E结尾。对于函数来说,还要加上参数列表在E后面,对于“int"类型就是字母”i“。

    可以使用c++filt来解析被修饰过的名字,如下:

    [linux]$ c++filt _ZN1N2C24hellEid
    	N::C2::hell(int, double)
    

    extern "C"
    C++会将在extern "C"的大括号里的代码当作C语言代码处理。

    3.13. 强符号与弱符号

    强符号不允许被多次定义。
    函数和初始化的全局变量(包括初始化为0)是强符号,未初始化的全局变量是弱符号。

    3.14. 成员默认初始化

    全局未初始化变量会被初始为0,而局部变量的值是不确定的。

    #include <iostream>
    
    int global;                         // global variable 初始化为0
    
    using namespace std;
    
    class Test{
        public:
            Test(){ 
                cout << _a << endl;  // member data 不确定
                int b;               // local variable 不确定
                cout << b << endl;
            }
            int a(){return _a;}
        private:
            int _a;
    };
    
    int main(){
        Test t;
        cout << t.a() << endl;                      // member data不确定
        int local;                                  // local variable 不确定
        cout << "global: " << global << endl;
        cout << "local:  " << local << endl;
    }
    

    第四章、静态链接

    4.1. 相似段合并

    将多个模块相同性质的段合并到一起。

    两步链接:
    一、空间与地址分配
    二、符号解析与重定位

    4.2. 全局构造与析构

    Linux下一般程序的入口是”_start“,这个函数是Linux系统库(Glibc)的一部分。在main函数之前,可能还有一些操作要被执行,比如全局对象的构造。

    .init 该段保存的指令在main被调用之前,Glibc的初始化部分执行这个段中的代码。
    .fini 同样,当一个程序的main函数正常退出时,Glibc会执行这个段中的代码。

    4.3. 编译库文件

    # 静态库
    ar -cr demo.a a.o b.o
    # 动态共享库
    gcc -fPIC -shared -o test.so test.c
    

    第五章、Windows PE/COFF

    第六章、可执行文件的装载与进程

    6.1. 进程虚拟地址空间

    操作系统占据高地址的1GB空间。剩下的是用户空间。

    6.2. 可执行文件的装载

    创建一个进程,装载相应的可执行文件并且执行。需要做三件事情:

    1. 创建一个独立的虚拟地址空间
    2. 读取可执行文件头,建立虚拟空间与可执行文件的映射关系
    3. 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行

    创建虚拟地址空间。并不是真正创建空间,实际上是创建映射函数所需的数据结构,在LInux上分配一个页目录就可以了,记录虚拟页与物理页帧间的对应关系。
    建立映射关系。上一步是虚拟空间到物理内存的映射,这一步是虚拟空间和可执行文件的映射。当发生页错误,知道从哪里读取数据进入内存。
    将指令寄存器设置成可执行文件入口,启动运行。

    6.3. Section合并

    将单个段(Section)装入内存由于页对齐的原因,会造成大量的空间浪费。这样,可以将相同权限的段(Section)合并装入,既可以达到权限管理,又可以节省空间。

    这里引入“Segment”的概念,如果将".text"段(Section),".init"段(Section)合并在一起看作是一个"Segment",那么装载的时候就可以看作一个整体一起映射,也就是说映射以后在进程虚存空间中只有一个相对应的VMA,而不是两个。

    如下,多个Section组成一个Segment,这些Section被映射到同一个VMA:

    $ readelf -l main
       Section to Segment mapping:
       Segment Sections...
       00     
       01     .interp 
       02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame .gcc_except_table 
       03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
       04     .dynamic 
       05     .note.ABI-tag .note.gnu.build-id 
       06     .eh_frame_hdr 
       07     
       08     .init_array .fini_array .jcr .dynamic .got 
    

    6.4 VMA

    一般进程按权限可以分为如下几种区:
    代码VMA、数据VMA、堆VMA、栈VMA。当讨论进程虚拟空间的”Segment“时,基本上就是这几种VMA。

    6.5 段(Segment)地址对齐

    页是物理内存调度的基本单位,页的大小一般为4096个字节。这样,由于Segment大小不一定对齐,也会造成空间的浪费。

    UNIX系统让各个段接壤部分共享一个物理页面,然后将该物理页面分别映射两次。

    第七章、动态链接

    7.1. 延迟绑定(Lazy Binding)

    有些函数直到程序运行结束也没有用到,把所有函数都链接好是一种浪费。延迟绑定的基本思想就是当函数第一次被用到时才进行绑定(符号查找、重定位),没有用到就不进行绑定。

    第八章、Linux共享库的组织

    8.1. SO-NAME

    Linux有一套机制可以保证库兼容问题。

    libname.so.x.y.z
    

    如上,x表示主版本号,y表示次版本号,z表示发行版本号。主版本号是软件是重大升级,不同主版本号之间是不兼容的。
    次版本号表示增量升级,会增加一些新的接口符号,且保持原来的符号不变。可以和相同主版本号的兼容。
    发行版本号表示库的一些错误的修正、性能的改进。

    在依赖动态库的软件中dynamic段会有DT_NEED的字段,字段的值就是需要的动态链接库名。

    SO-NAME是共享库文件名去掉次版本号和发布版本号,保留主版本号。比如一个共享库叫做libfoo.so.2.6.1,那么它的SO-NAME就是libfoo.so.2,它会链接到libfoo.so.2.6.1(一般会链接到最新版本)。

    第九章、Windows下的动态链接

    第十章、内存

    10.1. 程序的内存布局

    Alt text

    linux采用虚拟内存管理技术,每一个进程都有一个3G大小的独立的进程地址空间,这个地址空间就是用户空间内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。
    从高地址向下生长。
    从低地址向上生长。(不一定)

    10.2. 栈

    函数调用维护的信息:

    1. 函数的返回地址和参数
    2. 在函数调用前后需要保持不变的寄存器
    3. 局部变量

    10.3. 栈调用惯例

    调用方与被调用方共同遵守的约定。

    1. 函数参数的传递顺序:从左向右还是从右向左压入栈
    2. 栈的维护方式:弹出的工作由调用方还是函数完成
    3. 名字修饰(mangling)

    如下的函数:

    int _cdecl foo(int m, float n);
    

    按照VC中的cdecl标准,具体的栈操作如下:

    1. 将n压入栈
    2. 将m压入栈
    3. 调用_foo执行,分为两步:
      • 将返回地址栈
      • 跳转到_foo执行

    Alt text

    10.4. 函数返回值传递

    对于小于8字节的返回值,使用eax和edx联合返回的方式进行。如果大于8字节的类型,往往采用如下步骤:

    1. 在调用前,分配一个临时变量空间。
    2. 将临时变量的地址当作参数传入函数中。
    3. 函数中对临时变量进行操作,并将临时变量的地址放到eax中。
    4. 在调用函数后,将临时变量的值拷贝到目标变量中。

    10.5. 堆

    malloc的实现:
    有种做法,就是将进程的内存管理交给操作系统,每次申请内存,就一次系统调用,但系统调用开销很大,严重影响性能。比较好的做法是,进程向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间,具体来讲,管理堆空间分配的往往是程序的运行库
    运行库需要一个堆分配算法来管理申请的内存,不能把同一块地址分配两次。

    10.6. 向操作系统申请内存

    对于小于128KB的请求,它会在现有堆空间里面,按照堆分配算法为它分配一块空间并返回。对于大于128KB的请求来说,它会使用mmap()函数为它分配一块匿名空间,然后在匿名空间中为用户分配空间。

    10.7. 堆分配算法

    1. 空闲链表
      把空闲空间按照链表的方式连接起来,当用户请求空间时,遍历整个列表,直到找到合适大小的块并将它拆分;释放空间时,要将它加到空闲链表中。存在的问题:结构很脆弱,一旦链表被越界修改时,整个堆都无法工作。

    2. 位图
      将整个堆划分为大量的块,每个块大小相同。第一块称为头,其余是主体,可以使用一个数组记录块的使用情况,有头/主体/空闲三种状态。分配内存的时候容易产生碎片。

    3. 对象池
      每次分配的空间大小都一样,可以按照这个每次请求的分配的大小作为一个单位,把整个空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。

    glibc采用多种算法复合。

    第十一章、运行库

    第十二章、系统调用与API

    第十三章、运行库实现

  • 相关阅读:
    SQL 学习之路 (一)
    简单、易懂、通用的微信号、二维码轮播js
    本地phpstudy 新建站点运行步骤
    react-native 项目环境搭建
    JavaScript与DOM(下)
    JavaScript与DOM(上)
    ThisYes,this!
    编写高质量的JavaScript代码的基本要点
    变量对象(Variable Object)
    JavaScript核心
  • 原文地址:https://www.cnblogs.com/gr-nick/p/4395597.html
Copyright © 2011-2022 走看看