集合对象是一组无序排列的可哈希的值。
Python函数
1 返回值
当没有返回值时,python会返回一个None对象,当返回值数量超过一个时,默认返回一个元组(可以不加圆括号)。如果将多个返回值使用[]包含,那么返回一个列表。
映射类型:字典
字典类型和序列类型的区别在于存储和访问数据的方式不同。数列类型只保存值,可以将数字类型的顺序索引看作为键。而映射类型可以用其它对象类型作为键,比如数据串。在字典中,我们不用可序列化排序的键,所以映射类型的数据是无序的。
字典的常用操作
创建一个字典的方法有直接整体赋值、使用dict工厂方法和fromkeys函数。
Python中的浅拷贝和深拷贝
在Python语法基础中已经介绍了Python的内存管理方式,Python的赋值其实是简单的对象引用,同时通过对象计数的方式对对象进行管理。由此可以,当对一个对象进行拷贝到的时候,其实就是进行了简单的引用和计数加一。
浅拷贝实例
编译和连接-可执行文件的装载与进程
当我们需要执行一个可执行文件时,必须将它装载到内存以后才能被CPU执行,同时操作系统会为其运行一个进程,同时进程存在一个虚拟地址空间,可执行文件与虚拟地址空间存在一种映射关系。
进程虚拟地址空间
程序是一个静态的概念,他是一个指令集合文件。而进程是一个动态的概念,他是程序运行时的一个过程和操作系统管理的结构。
每个程序运行以后,他都拥有自己独立的虚拟地址空间,这个空间的大小由计算机的硬件决定(硬件寻址空间),如32位的计算机的虚拟地址空间为4G。
装载的方式和过程
程序执行时所需要的指令和数据必须在内存中才能正常运行,如果仅仅是将全部指令和数据都放入内存,物理内存的数量往往不够。由于程序运行的局部原理,我们可以将常用的驻留内存,其他的放在磁盘中,使用时再去磁盘中取。
一般解决这个问题的方法是页映射方法,对内存的使用基于页进行分配,操作系统通过一个页目录的结构对内存进行管理。关于页表的问题可以参考操作系统相关书籍。
进程的建立过程:
- 创建一个独立的虚拟地址空间。不需要创建物理空间,只需要创建虚拟空间与物理空间的映射函数,一般是一个页目录。
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。文件头里面有可执行文件的相关信息,这样当需要访问某一页而这一页不在物理空间,就可以通过页错误去磁盘中的可执行文件中查找。
- 将CPU的指令寄存器设置为可执行文件的入口地址,启动运行。文件头中有这个入口地址信息。
进程虚存空间分布
在执行文件中,常常有非常多的段,如果每个段在内存中都映射为一个段,因为每个段都要进行页的对齐,那将会浪费非常多的碎片空间。因此,操作系统根据访问权限将段合并为相应的segment,段根据访问权限可分为三类:可读可执行段(如代码段)、可读可写段(数据段和bbs段)、只读数据段(常数段、字符串段等)。同时考虑堆和栈,进程虚拟空间如下所示:
Linux将进程虚拟空间中的一个段叫虚拟内存区域(VMA,Virtual Memory Area);Windows中叫这个虚拟段。
进程虚拟地址空间概念总结:
- 操作系统通过给进程空间划分出一个个VMA进行管理进程的虚拟空间;
- 基本原则是将相同权限、相同映像文件的映射成一个VMA;
- 一个进程基本上可分为四类VMA区域:
- 代码VMA:只读、可执行;有映像文件。
- 数据VMA:权限可读写、可执行;有映像文件。
- 堆VMA:权限可读写、可执行;无映像文件;向上扩展。
- 栈VMA:可读写、不可执行;无映像文件;向下扩展。
编译和连接-静态连接
静态链接是将两个文件(他们之间存在引用)合并为一个文件,并将他们的引用地址进行重定位。主要分为两步:1.空间与地址分配。2.符号解析和重定位。
我们将源代码a.c和b.c作为例子分析:
a.c
extern int shared;
int main(){
int a=100;
swap(&a,&shared);
}
b.c
int shared=1;
void swap(int *a,int *b){
int t=*a;
*a=*b;
*b=t;
}
编译后获得目标文件a.o和b.o,连接是将这两个目标文件作为输入,生成最终的可执行文件。
空间和地址分配
我们已经介绍了目标文件的格式,那么,如何将多个目标文件声称可执行文件呢,当前的方法是将相同性质的段合并到一起。如下图所示。
对于空间分配,其实有两个含义:一是在输出的可执行文件中的空间。二是装载后的虚拟地址中的虚拟地址空间。对于有实际数据的段如.text和.data,文件中和虚拟地址中都要分配空间;而对于.bbs这种段,文件中没有内容,分配空间的意义在于虚拟地址空间。实际上,我们谈到的空间分配只关注与虚拟地址空间的分配,因为这关系到后面的连接器的地址计算过程。
综上所述,这一步的功能和步骤如下:扫描所有的输入目标文件,获得他们每个段的长度、属性和位置,并将输入目标文件中的符号表中的所有符号定义和符号引用收集起来,统一放到一个全局符号表。这一步连接器合并所有输入文件的段,并计算输出文件中各段合并后的长度和位置。
简单地说,就是:基于多个输入的目标文件生成一个新的ELF格式可执行文件,要求该文件符合语义要求。
连接器所使用的地址已经是程序在进程中的虚拟地址(用于重定位),而忽略了文件偏移。连接生成的执行文件ab各个段被分配到相应的虚拟地址,如上图.text段分配到0X08048094,大小为0X72字节。.data段地址为0x08049108,大小为4字节。如下图所示。具体的分配地址策略和操作系统有关,如Linux下ELF可执行文件默认从地址0x08048000可是分配。
在确定了每个段的的虚拟地址之后,各个符号的位置就固定了,如main、shared、swap等,只要将他们相对于段的偏移量加上段的起始位置即可。
符号解析和重定位
在编译完成后,连接开始前,在目标文件a.o中虽然有引用了shared和swap,但是它不知道它们具体的实际地址,因此就先用未定义数字填充这些地址,如使用0看作变量shared的地址,将-4看为函数swap的地址,这只是一个临时地址。
在连接以后的ab文件中,shared地址和swap的地址都已经确定,因此链接将这些临时地址替换为真实的实际地址。
在目标文件中,有一个重定位表的结构来保存那些需要重定位的符号,每一个段中如果有需要重定位的符号都会有一个重定位表段,如.tetx段中存在需要重定位的信息,就会有一个.rel.text的段保存重定位表。相应的,.data对应.rel.data。
重定位表中存放两个信息:偏移量和信息。偏移量表示重定位的入口再被重定位的段中的位置,而信息存放重定位的类型和符号。
编译和链接-目标文件格式
目标文件是编译器编译源代码后生成的文件,从结构上讲,它具有可执行文件的文件格式,但是没有经过连接过程,因此对于其他编译模块的引用还没有连接上,本文将简单介绍目标文件的文件格式,对目标文件格式的理解是下文介绍静态链接和动态链接的基础。
可执行文件格式类型
目标文件是源代码编译后的文件(Windows下的.obj和Linux下的.o),他和可执行文件的格式是相同的,可执行文件格式主要是Windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format)。
除此之外,动态链接库(DLL, Dynamic Linking Library)和静态链接库(Static Linking Library)也是可执行文件格式存储的。ELF格式的文件归为一下四类:
ELF文件类型 | 说明 | 实例 |
目标文件 | 可连接为可执行文件或共享目标文件,静态链接库也属于此类 | Linux:.o Windows: .obj |
可执行文件 | 可直接执行的文件 | Bin/bash文件 Exe文件 |
共享目标文件 | 动态链接库 | .so .dll |
核心转储文件 | ? | ? |
目标文件格式
在目标文件中,需要存储编译后的机器指令的指令代码以及数据,还有连接需要的信息,如符号表、字符串等。目标文件将不同的信息放在不同的地方,分"段"(segment)存储。
如下图所示,目标文件中最基本最重要的有以下几个段:代码放在代码段(.text段)中,全局变量和静态变量放在数据段(data段)中,而为初始化的全局和静态变量定义放在.bbs段中,因为他们都默认为0,表示为为初始化的全局变量和静态变量预留位置。
图 1
综上所述,程序源代码代码主要分为两种段:程序指令和程序数据。原因和好处有:
- 可设为一个只读,一个可读写。
- 提高程序的局部性,提高缓存的命中率。
- 同时运行多个程序副本时,只需要存储一个程序指令的代码段数据。
ELF文件结构描述
上文是一个ELF文件(目标文件属于ELF文件)的大致介绍,下文将详细介绍ELF文件的格式。下图描述了ELF的总体结构,本图只显示一些重要的结构。
文件头
ELF最前面的还是ELF文件头,描述了整个文件的基本属性,如ELF魔数、文件机器字节长度、数据存储方式、ELF重定位类型、硬件平台、入口地址、程序头入口、段表的位置和长度以及段的数量。
段表
ELF文件中有各种各样的段,而段表(Section Header table)保存了这些段的基本属性,如段名、长度、文件中的偏移等。段表的位置由头文件中的信息确定,而其他段的位置由段表确定。
重定位表
在我们举的对象文件的例子(图1)中,会产生一个".rel.text"的段,它就是一个重定位表。连接器在处理目标文件时,目标文件对其他模块的对象的引用需要进行重定位,这些信息都记录在ELF文件的重定位表中。对于每一个需要重定位的代码段或者数据段,都有一个相应的重定位表。比如".rel.text"就是对".text"段的重定位表,如我们的例子中有一个对printf函数的调用,因此.text段有一个绝对地址的引用。
重定位表在链接过程中具有重要的意义。
字符串表
ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串长度不同,为了使用固定的结构表示,ELF文件将字符串集中起来存放,然后使用字符串在表中的偏移量来引用字符串。
常见的段名为.strtab和.shstrtab,分别代表字符串表和段表字符串表,前者保存普通的字符串,后者保存段名。
符号——链接的接口
链接的本质是将相互调用的目标文件进行组装。如目标A文件中的函数f()被目标文件B调用,连接就是将他们连接起来。在链接中,我们将函数和变量成为符号,他们的名字是符号名。
每一个目标文件都会有一个符号表,记录了目标文件中所使用的所有文件。每个符号有一个对应的符号值,对于变量和函数来说,符号值就是他们对应的地址。
符号主要有以下几种:
- 定义在目标文件的全局符号,可以被其他目标文件引用。如全局变量和函数。
- 本目标文件中引用的其他文件的全局符号,一般叫外部符号。如printf函数。
- 段名,符号由编译器产生,它的值就是起始地址。如.text.
- 局部符号,只在编译单元内可见。如局部static变量。
- 行号信息。
其中,链接过程最关心全局符号的粘合,所以最重要的就是第一类和第二类全局信息
符号表一般是ELF文件的一个段.symtab。每个符号有一个结构,存储的信息包括:符号名、符号值、符号大小、符号类型和绑定信息,符号所在段。
其中,符号类型有:
NOTTYPE | 未知符号类型 |
OBJECT | 数据对象,如变量、数组 |
FUNC | 函数 |
SECTION | 段 |
FILE | 文件名。 |
绑定信息有
LOCAL | 局部符号 |
GLOBAL | 全局符号 |
WEAK | 弱引用 |