###《程序员的自我修养》

摘要:
程序员的自我培养。以换入和换出为单位,大大减少了换入和换出的数据。程序中的地址是虚拟地址,通过MMU转换为物理地址。这4个字节称为ELF文件的幻数。01表示ELF文件的主要版本号,通常为1,因为ELF标准自版本1.2以来未更新:-)。它是“.text”段的重新定位表。

程序员的自我修养。

#@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

第十三章、运行库实现

免责声明:文章转载自《###《程序员的自我修养》》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇STDMETHOD (转)Yii2中JSONP跨域问题的解决下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

insmod过程详解【转】

转自:http://blog.csdn.net/chrovery/article/details/51088425 转自http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=27717694&id=3971861 一、前言 对于现在编译的一些module要insmod在系统上时...

binary hacks读数笔记(readelf基本命令)

一、首先对readelf常用的参数进行简单说明: readelf命令是Linux下的分析ELF文件的命令,这个命令在分析ELF文件格式时非常有用,下面以ELF格式可执行文件test为例详细介绍: 1、readelf -v 显示版本 2、readelf -h 显示帮助 3、readelf -a test 显示test的全部信息 4、readelf -h te...

Android合并文件的三种方式代码

amr格式的文件头是6字节,在进行文件合并的时候要减去除第一个文件以外的其他文件的文件头。下面介绍合并文件的几种方式,并通过合并amr文件来举例介绍合并文件的具体流程。 注意:不同文件的文件头是不一样的,所以在合并的时候根据不同文件相应的减去合并文件的文件头。具体你可以学习Android开发教程。 步骤一:获取要合并的文件及创建合并后保存的文件 /**用于...

mac下的readelf和objdump

ELF文件包括: (1)可重定位的目标文件 (2)可执行的目标文件 (3)可被共享的目标文件 可以用file命令来看目标文件是否是ELF文件 在linux下,用readelf来看ELF头部或者其它各section的内容,用objdump来对指定的内容(.text, .data等)进行反汇编。 但是mac os X下没有这两个命令,可以用brew来安装,...

文件上传漏洞(File Upload)

简介 File Upload,即文件上传漏洞,通常是由于对用户上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马,病毒,恶意脚本等获取服务器的webshell权限,并进而攻击控制服务器,因此文件上传漏洞带来的危害常常是毁灭性的。简单点说,就是用户直接或者通过各种绕过方式将webshell上传到服务器中进而执行利用。例如,如果你的服务器...

BMP图像数据格式详解

一.简介 BMP(Bitmap-File)图形文件是Windows采用的图形文件格式,在Windows环境下运行的所有图象处理软件都支持BMP图象文件格式。Windows系统内部各图像绘制操作都是以BMP为基础的。Windows 3.0以前的BMP图文件格式与显示设备有关,因此把这种BMP图象文件格式称为设备相关位图DDB(device-dependent...