<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>StephenChan&#039;s Tech Space &#187; Operating System</title>
	<atom:link href="http://blog.endlesscode.com/category/operating-system/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.endlesscode.com</link>
	<description>Stay Hungry. Stay Foolish.</description>
	<lastBuildDate>Tue, 25 Oct 2011 01:15:07 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>动态链接</title>
		<link>http://blog.endlesscode.com/2010/06/07/dynamic-linkage/</link>
		<comments>http://blog.endlesscode.com/2010/06/07/dynamic-linkage/#comments</comments>
		<pubDate>Mon, 07 Jun 2010 11:44:54 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=812</guid>
		<description><![CDATA[动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分，在程序运行时才将它们链接在一起形成一个完整的程序，而不是像静态链接那样把所有的程序模块都链接成一个单独的可执行文件。动态链接涉及运行时的链接及多个文件的装载，必需要有操作系统的支持，因为动态链接的情况下，进程的虚拟地址空间的分布会比静态链接情况下更为复杂，还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙的变化。在Linux系统中，ELF动态链接文件被称为“动态共享对象（DSO，Dynamic Shared Objects），简称共享对象，它们一般都是以“.so”为扩展名的一些文件，而在Windows系统中，动态链接文件被称为动态链接库（Dynamical Linking Library），即平时见到的以“.dll”为扩展名的文件。 动态链接库的特点与优势 把函数库推迟到程序运行时加载的好处有几个： 可以实现进程之间的资源共享。就是说，某个程序的在运行中要调用某个动态链接库函数的时候，操作系统首先会查看所有正在运行的程序，看在内存里是否已有此库函数的拷贝。如果有，则让其共享那一拷贝（共享代码段，数据码各自独立一份）；只有没有才链接载入。这种模式虽然会带来一些”动态链接“额外的开销，但却大大地节省了系统的内存资源，通过一定的优化，与静态链接相比，性能损失大约在5％以下。 程序升级变得简单。用户只需要升级动态链接库，而无需像静态链接那样重新编译其他原有的代码就可以完成整个程序的升级。 可以使链接载入由程序员在程序代码中控制，如dlopen、dlsym、dlclose等等。 装载重定位 静态链接是通过在链接过程对所有目标文件进行重定位的。而对于动态链接库而言，在链接时，可执行文件对所有绝对地址的引用不作重定位，而把这一步推迟到装载时再完成。一旦模块装载地址确定，即目标地址确定，那么系统对程序中所有的绝对地址引用进行重定位。负责完成这部分工作的是动态链接器（Dynamic Loader，其实也是一个共享对象，ld.so）。 动态链接模块被装载遇到至虚拟空间之后，指令部分是在多个进程之间共享的，由于装载时重定位的方法需要修改指令，所以没有办法做到同一份指令被多个进程共享，因为指令被重定位后对于每个进程来讲是不同的。当然，动态链接库中的可修改数据部分对于不同的进程来说有多个副本，所以它们可以采用装载时重定位的方法来解决。 因此，地址无关代码（PIC，Position-independent Code）就在这种需求的前提产生了。这种方案就是把指令中那些需要被修改的部分分离出来，跟数据部分放在一起，这样指令部分就可以保持不变，而数据部分可以在每个进程中拥有一个副本。 可以按是否跨模块和引用方式分为4类地址引用类型： /* a.elf */ static int a; extern int b; extern void ext(); void bar() { a = 1; //类型2 模块内数据访问 b = 2; &#8230; <a href="http://blog.endlesscode.com/2010/06/07/dynamic-linkage/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分，在程序运行时才将它们链接在一起形成一个完整的程序，而不是像静态链接那样把所有的程序模块都链接成一个单独的可执行文件。动态链接涉及运行时的链接及多个文件的装载，必需要有操作系统的支持，因为动态链接的情况下，进程的虚拟地址空间的分布会比静态链接情况下更为复杂，还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙的变化。在Linux系统中，ELF动态链接文件被称为“动态共享对象（DSO，Dynamic Shared Objects），简称共享对象，它们一般都是以“.so”为扩展名的一些文件，而在Windows系统中，动态链接文件被称为动态链接库（Dynamical Linking Library），即平时见到的以“.dll”为扩展名的文件。</p>
<h4>动态链接库的特点与优势</h4>
<p>把函数库推迟到程序运行时加载的好处有几个：</p>
<ul>
<li>可以实现进程之间的资源共享。就是说，某个程序的在运行中要调用某个动态链接库函数的时候，操作系统首先会查看所有正在运行的程序，看在内存里是否已有此库函数的拷贝。如果有，则让其共享那一拷贝（共享代码段，数据码各自独立一份）；只有没有才链接载入。这种模式虽然会带来一些”动态链接“额外的开销，但却大大地节省了系统的内存资源，通过一定的优化，与静态链接相比，性能损失大约在5％以下。</li>
<li>程序升级变得简单。用户只需要升级动态链接库，而无需像静态链接那样重新编译其他原有的代码就可以完成整个程序的升级。</li>
<li>可以使链接载入由程序员在程序代码中控制，如dlopen、dlsym、dlclose等等。<span id="more-812"></span></li>
</ul>
<h4>装载重定位</h4>
<p>静态链接是通过在链接过程对所有目标文件进行重定位的。而对于动态链接库而言，在链接时，可执行文件对所有绝对地址的引用不作重定位，而把这一步推迟到装载时再完成。一旦模块装载地址确定，即目标地址确定，那么系统对程序中所有的绝对地址引用进行重定位。负责完成这部分工作的是动态链接器（Dynamic Loader，其实也是一个共享对象，ld.so）。</p>
<p>动态链接模块被装载遇到至虚拟空间之后，指令部分是在多个进程之间共享的，由于装载时重定位的方法需要修改指令，所以没有办法做到同一份指令被多个进程共享，因为指令被重定位后对于每个进程来讲是不同的。当然，动态链接库中的可修改数据部分对于不同的进程来说有多个副本，所以它们可以采用装载时重定位的方法来解决。</p>
<p>因此，地址无关代码（PIC，Position-independent Code）就在这种需求的前提产生了。这种方案就是把指令中那些需要被修改的部分分离出来，跟数据部分放在一起，这样指令部分就可以保持不变，而数据部分可以在每个进程中拥有一个副本。</p>
<p>可以按是否跨模块和引用方式分为4类地址引用类型：</p>
<pre class="brush:c++">/* a.elf */
static int a;
extern int b;
extern void ext();

void bar()
{
    a = 1;  //类型2 模块内数据访问
    b = 2;  //类型3 模块间数据访问
}

void foo()
{
    bar();  //类型1 模块内调用或跳转
    ext();  //类型4 模块间调用或跳转
}</pre>
<p><strong>类型一 模块内部调用或跳转</strong></p>
<p>对于现代的操作系统来讲，模块内部的跳转、函数调用都可以是相对地址调用，或者是基于寄存器相对调用，所以对于这种指令是不需要重定位的，这点从静态链接时的模块内部重定位可以看出。</p>
<p><strong>类型二 模块内部数据访问</strong></p>
<p>一个模块前面一般是若干个页的代码，后面紧跟着若干个页的代码，这些页之间的相对位置是固定的，也就是说，任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的，那么只需要对于<strong>当前指令的地址</strong>加上<strong>固定的偏移量</strong>就可以访问模块内部数据了。现代的体系结构中，数据的相对寻址往往没有相对于当前指令地址（PC）的寻址方式，所以ELF用了一个很巧妙的方法来得到当前的PC值，然后再加上一个偏移量就可以达到访问相应变量的目的了。</p>
<p>其实就是在代码中调用了一个函数（__i686.get_pc_thunk.cx），在这个函数中把esp（堆栈）中存放的返回地址存到某个寄存器中，然后再原来的代码中从寄存器（此处为ecx）中取出来。反汇编出来的结果如下：</p>
<pre class="brush:plain">000004a7 &lt;__i686 .get_pc_thunk.cx&gt;:
 4a7:	8b 1c 24          mov    (%esp),%ecx
 4aa:	c3                   	ret
 4ab:	90                   	nop
<!--__i686--></pre>
<p><strong>类型三 模块间数据访问</strong></p>
<p>模块间的数据访问比模块内部稍微麻烦，因为模块间的数据访问目标地址要等到装载时才决定的。比如上面的变量b，它被定义在其他模块中，并且该地址在装载时才能确定，因此，这时就需要用到了在&#8221;a.elf&#8221;文件的数据段里面建立一个指向这些变量的指针数组（注意是在&#8221;a.elf&#8221;的数据段），也被称为全局偏移表（GOT，Global Offset Table）。</p>
<p>全局偏移表是将地址无关代码（PIC）通过计算转换成相应的绝对地址。当代码需要引用该全局变量时，可以通过GOT中相对应的项间接引用来获取到绝对地址。而在重定位项的类型是R_386_GLOB_DAT，其实就是对全局偏移表的引用。</p>
<p>在指令要访问变量b时，程序会先找到当前目标文件的GOT，然后根据GOT中变量所对应的项找到变量的绝对地址。每个变量都对应一个4个字节的地址，链接器在装载模块的时候会查找每个变量的绝对地址，然后填充GOT中的各个项，以确保每个指针所指向的地址正确。由于GOT本身是放在数据段的，所以它可以在模块装载时被修改，并且每个进程都可以有独立的副本，相互不受影响。</p>
<p>将上面的代码编译成so文件，可以看到got的属性是一个数据段：</p>
<pre class="brush:plain">pic.so:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
 18 .got          00000010  00001fe4  00001fe4  00000fe4  2**2
                      CONTENTS, ALLOC, LOAD, DATA</pre>
<p>在动态链接器将控制权交给进程映像中任何的代码之前，会处理所有的全局偏移表重定位项，确保执行过程中绝对地址信息可用。这个过程可以大概描述如下：</p>
<ol>
<li>动态链接器开始处理重定位项，处理到全局变量b。</li>
<li>从重定位表项中的r_info低8位判断重定位项的类型是R_386_GLOB_DAT，从r_info的高24位获取到重定位入口的符号表符号表的位置。</li>
<li>从重定位表项中的r_offset中获取到了全局变量b在&#8221;a.elf&#8221;全局偏移表GOT的位置。</li>
<li>根据从r_info的高24位获取到的下标在全局符号表中获取到了全局变量b的地址，经过重定位计算后将b的绝对地址更新到步骤3中得知的GOT位置。</li>
<li>当在&#8221;a.elf&#8221;中的代码指令要访问b时，程序在编译的时候已经在访问b的位置像类型一那样安插一段代码，也像类型一那样通过当前的PC值加上一偏移量计算出b在GOT中的位置，然后再从该位置获取到b的绝对地址。</li>
</ol>
<p><strong>类型四 模块间调用或跳转</strong></p>
<p>这种完全可以使用类型三的方法可以解决，但是使用这种方法会使性能存在一些问题。</p>
<p>至于共享模块的全局变量冲突问题，ELF共享库在编译时，默认都把定义在模块内部的全局变量当作定义在其他模块的全局变量，也就是说当共享模块被装载时，如果某个全局变量在可执行文件中拥有副本，那么动态链接器就会把GOT中的相应地址指向该副本，这样该变量在运行时实际上最终就只有一个实例。</p>
<p>而对于取地址这种重定位入口：</p>
<pre class="brush:c">static int a;
static int* p = &amp;a;</pre>
<p>实际上这样在数据段中绝对地址的引用，相应地对于p的重定位项类型为&#8221;R_386_RELATIVE&#8221;。</p>
<h4>延迟绑定</h4>
<p>动态链接的确但静态链接灵活很多，但是同时也比静态链接慢，因为动态链接下的模块间变量访问和函数调用都要进行间接处理，而且在程序启动的时候要寻找并装载所需要的共享对象，并进行符号查找和地址重定位等工作势必减慢程序的启动速度。因此，ELF采用了一种叫延迟绑定（Lazy Binding）的做法，基本的思想就是当函数第一次被用到时才进行绑定（包括符号查找、重定位等），如果没有用到则不进行绑定。这样的做法可以大大加快程序的启动速度，特别有利于一些有大量函数引用和大量模块的程序。</p>
<p>ELF文件将GOT拆分成两个表叫做&#8221;.got&#8221;和&#8221;.got.plt&#8221;。其中&#8221;.got&#8221;用来保存全局变量引用的地址，&#8221;.got.plt&#8221;用来保存函数引用的地址，也就是说，所有对于外部函数的引用全部被分离出来放到了&#8221;.got.plt&#8221;。</p>
<p>当我们调用某个外部模块的函数时，如果按照通常的做法应该是通过GOT中相应的项进行间接跳转。PLT为了实现延迟绑定，在这个过程中间又增加了一层间接。调用函数并不直接通过&#8221;.got.plt&#8221;跳转，而是通过一个叫作&#8221;.plt&#8221;的结构来进行跳转。每个外部函数在&#8221;.plt&#8221;中都有一个相应的项，比如bar()函数在&#8221;.plt&#8221;中的项的地址可以称为&#8221;bar@plt&#8221;。&#8221;.plt&#8221;的基本结构大概如下（以bar@plt为例）：</p>
<pre class="brush:asm">.PLT0:
push *(.got.plt+4)
jump *(.got.plt+8)

.PLT1:
jump *(bar@got.plt)
push offset_of_rel
jump .PLT0</pre>
<p>延迟绑定的符号解析过程大概如下（以上面示例中的bar()为例）：</p>
<ol>
<li>代码中调用bar()的地方在编译时已经被替换成一条调用bar@plt的转移指令，即是调用的&#8221;.plt&#8221;中的bar所在的项,即&#8221;.PLT1&#8243;。</li>
<li>当首次解析的时候，虽然在&#8221;.got.plt&#8221;中有bar函数的项，但是在没有解析之前，该项存储的内容为0，而非bar函数的绝对地址，因此指令&#8221;jump *(bar@got.plt)&#8221;实际上没有任何的动作，只是直接跳转到下一条push指令（因为jump的偏移为0）。</li>
<li>指令&#8221;push offset_of_rel&#8221;是将bar()函数的重定位项的索引压栈，而该重定位项的类型为R_386_JMP_SLOT，offset_of_rel是重定位表&#8221;.rel.plt&#8221;中的下标。</li>
<li>接着指令&#8221;jump .PLT0&#8243;跳转到.PLT0，将&#8221;.got.plt&#8221;的第二表项压栈，然后再跳转到&#8221;.got.plt&#8221;第三个表项指定的地址。这里要简述一下&#8221;.got.plt&#8221;的结构，&#8221;.got.plt&#8221;和&#8221;.got&#8221;几乎是一样的，除了&#8221;.got.plt&#8221;前三项的值有特殊意义：(1)第一项保存&#8221;.dynamic&#8221;段的地址，这个段描述了本模块动态链接相关的信息（可暂忽略）；(2)保存本模块的ID（用于在动态链接时查找本模块的信息）；(3)保存的是_dl_runtime_resolve()函数的地址，用于启动动态链接器，加载模块，解析符号并重定位。。</li>
<li>此时，可以看到在堆栈上，已经存有bar()函数在重定位项的索引和当前要重定位的模块ID，那么_dl_runtime_resolve()函数就可以启动动态链接器，当动态链接器得到控制后，它恢复堆栈，获取在步骤3中入栈的重定位项索引和步骤4中入栈的本模块信息，通过这2个信息，动态链接器符号解析和计算绝对地址，将bar()的“真实”地址存储于bar()在全局偏移表&#8221;.got.plt&#8221;的表项（原来的值为0）。</li>
<li>动态链接器更新了&#8221;.got.plt&#8221;中的信息后，将执行的控制权传递给bar()函数。那么当再次调用bar()函数时，PC依然会跳转到&#8221;.PLT1&#8243;，但是此时&#8221;bar@got.plt&#8221;的值已经不再是0了，因为会正确地跳转到bar()函数的绝对地址。</li>
</ol>
<h4>动态链接相关的数据结构</h4>
<p>在动态链接的情况下，可执行文件的装载与静态链接情况基本一样。首先操作系统会读取可执行文件的头部，检查文件合法性，然后从头部中的&#8221;Program Header&#8221;中读取每个&#8221;Segment&#8221;的虚拟地址、文件地址和属性，并将它们映射到进程虚拟空间的相应位置，这些步骤跟静态链接情况下基本一样。但不同的是，静态链接在完成以上工作后会将控制权直接转交给可执行文件的入口地址(e_entry)，然后程序开始执行。而动态链接情况下，操作系统需要加载可执行文件依赖的所有共享对象，此时还有很多外部符号的引用还处于无效地址的状态，因此操作系统会启动一个动态链接器，可加载可执行文件所依赖的共享对象，最后才将控制权转交给可执行文件。</p>
<p><strong>&#8220;.interp&#8221;段</strong></p>
<p>这个段的内容是指定可执行文件所需要的动态链接器的路径，即是说动态链接器的位置即不是系统配置指定的，也不是由环境参数决定的，而是由ELF可执行文件决定的。</p>
<pre class="brush:plain">Contents of section .interp:
 8048134 2f6c6962 2f6c642d 6c696e75 782e736f  8048144 2e3200 /lib/ld-linux.so.2</pre>
<p>其实这个只是一个软链接。动态链接器在Linux下是Glibc的一部分，也就是属于系统库级别的，它的版本号往往跟系统中的Glibc库版本号一样的。</p>
<p><strong>&#8220;.dynamic&#8221;段</strong></p>
<p>动态链接ELF中最重要的结构应该是&#8221;.dynamic&#8221;段，这个段里面保存了动态链接器所需要的基本信息，比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等，所以，&#8221;.dynamic&#8221;段可以看成是动态链接下ELF文件的”文件头“。</p>
<pre class="brush:c">typedef struct {
    Elf32_Sword d_tag;
    union {
        Elf32_Word d_val;
        Elf32_Addr d_ptr;
    } d_un;
 } Elf32_Dyn;</pre>
<p>&#8220;.dynamic&#8221;段的结构基本上是由一个类型值加上一个附加的数值或指针。而附加的union值可能是索引、下标和虚拟地址等等。而类型为DT_NEED类型则为动态链接的模块所依赖的模块路径。</p>
<p><strong>&#8220;.dynsym&#8221;段</strong></p>
<p>这个段称为动态符号表（Dynamic Symbol Table），用来表示动态链接这些模块之间的符号导入导出关系。与&#8221;.symtab&#8221;不同的是，&#8221;.dynsym&#8221;只保存了与动态链接相关的符号，对于那些模块内部的符号，比如模块私有变量则不保存。很多时候动态链接的模块同时拥有&#8221;.dynsym&#8221;和&#8221;symtab&#8221;两个表，&#8221;.symtab&#8221;中往往保存了所有符号，包括&#8221;.dynsym&#8221;中的符号。</p>
<p><strong>动态链接的重定位表</strong></p>
<p>在静态链接中，目标文件里面包含有专门用于表示重定位信息的重定位表，比如&#8221;.rel.text&#8221;表示是代码段的重定位表，&#8221;.rel.data&#8221;是数据段的重定位表。在相应在动态链接文件中，也有类似的重定位表分别叫做&#8221;.rel.dyn&#8221;和&#8221;.rel.plt&#8221;，它们分别相当于&#8221;.rel.data&#8221;和&#8221;.rel.plt&#8221;。</p>
<p>&#8220;.rel.dyn&#8221;是对数据引用进行修正，它所修正的位置位于&#8221;.got&#8221;以及数据段；而&#8221;.rel.plt&#8221;是对函数引用的修正，它所修正的位置位于&#8221;.got.plt&#8221;。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/06/07/dynamic-linkage/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>ELF文件格式说明</title>
		<link>http://blog.endlesscode.com/2010/06/06/elf-file-format-details/</link>
		<comments>http://blog.endlesscode.com/2010/06/06/elf-file-format-details/#comments</comments>
		<pubDate>Sat, 05 Jun 2010 18:24:51 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=814</guid>
		<description><![CDATA[ELF文件类型 ELF文件主要分为三种类型: 可重定位文件（Relocatable File）包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。 可执行文件（Executable File） 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。 共享目标文件（Shared Object File） 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。 ELF文件的数据表示 ELF文件头结构及相关常数被定义在&#8221;/usr/include/elf.h&#8221;里。ELF目标文件中的所有数据结构都遵从自然大小和对齐规则。如果必要，数据结构可以包含显式的补齐，例如为了确保4字节对象按4字节边界对齐。数据对齐同样适用于文件内部。下面为ELF中常用的数据格式： 名称 大小 对齐 描述 Elf32_Addr 4 4 无符号程序地址 Elf32_Half 2 2 无符号短整型 Elf32_Off 4 4 无符号偏移地址 Elf32_Sword 4 4 有符号整型 Elf32_Word 4 4 有符号整型 ELF除了32位版还有64位版本，数据类型的名称和大小也相应地变化（Elf64_Addr&#8230;)。 &#8230; <a href="http://blog.endlesscode.com/2010/06/06/elf-file-format-details/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<h4>ELF文件类型</h4>
<p>ELF文件主要分为三种类型:</p>
<ul>
<li> 可重定位文件（Relocatable File）包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。</li>
<li> 可执行文件（Executable File） 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。</li>
<li> 共享目标文件（Shared Object File） 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。</li>
</ul>
<h4>ELF文件的数据表示</h4>
<p>ELF文件头结构及相关常数被定义在&#8221;/usr/include/elf.h&#8221;里。ELF目标文件中的所有数据结构都遵从自然大小和对齐规则。如果必要，数据结构可以包含显式的补齐，例如为了确保4字节对象按4字节边界对齐。数据对齐同样适用于文件内部。下面为ELF中常用的数据格式：</p>
<table style="margin: 0pt auto; width: 300px;" border="1">
<tbody>
<tr>
<td><strong>名称</strong></td>
<td><strong>大小</strong></td>
<td><strong>对齐</strong></td>
<td><strong>描述</strong></td>
</tr>
<tr>
<td>Elf32_Addr</td>
<td>4</td>
<td>4</td>
<td>无符号程序地址</td>
</tr>
<tr>
<td>Elf32_Half</td>
<td>2</td>
<td>2</td>
<td>无符号短整型</td>
</tr>
<tr>
<td>Elf32_Off</td>
<td>4</td>
<td>4</td>
<td>无符号偏移地址</td>
</tr>
<tr>
<td>Elf32_Sword</td>
<td>4</td>
<td>4</td>
<td>有符号整型</td>
</tr>
<tr>
<td>Elf32_Word</td>
<td>4</td>
<td>4</td>
<td>有符号整型</td>
</tr>
</tbody>
</table>
<p>ELF除了32位版还有64位版本，数据类型的名称和大小也相应地变化（Elf64_Addr&#8230;)。<span id="more-814"></span></p>
<h4>链接视图和执行视图</h4>
<p>目标文件既要参与程序链接又要参与程序执行。出于方便性和效率考虑，目标文件格式提供了两种并行视图，分别反映了这些活动的不同需求。</p>
<p><img class="aligncenter size-full wp-image-820" title="exec_view" src="http://blog.endlesscode.com/wp-content/uploads/2010/06/exec_view.jpg" alt="exec_view" width="414" height="297" /></p>
<p>这两个视图并不冲突，在执行视图中的&#8221;Segment&#8221;是由链接视图中的多个权限和性质相仿的&#8221;Section&#8221;组成的。</p>
<p>文件开始处是一个ELF头部（ELF Header），用来描述整个文件的组织。ELF的文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等信息。</p>
<p>程序头部表（Program Header Table），主要用来保存&#8221;Segment&#8221;的信息。可重位文件由于不需要被装载，因此没有这个表，而ELF可执行文件和共享库文件则有。</p>
<p>段表（Section Header Table），保存各个段的基本属性，包括每个段的段名、段的长度、在文件中的偏移、读写权限、段的链接信息等等。</p>
<h4>ELF Header 部分</h4>
<p>下面是使用readelf来查看一个ELF文件的文件头信息：</p>
<pre class="brush:plain">ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                                          ELF32
  Data:                                           2's complement, little endian
  Version:                                      1 (current)
  OS/ABI:                                       UNIX - System V
  ABI Version:                                0
  Type:                                          REL (Relocatable file)
  Machine:                                     Intel 80386
  Version:                                      0x1
  Entry point address:                     0x0
  Start of program headers:           0 (bytes into file)
  Start of section headers:              264 (bytes into file)
  Flags:                                          0x0
  Size of this header:                      52 (bytes)
  Size of program headers:            0 (bytes)
  Number of program headers:      0
  Size of section headers:               40 (bytes)
  Number of section headers:         11
  Section header string table index: 8</pre>
<p>文件头对应一个Elf32_Ehdr的结构如下（如上面的输出信息是一一对应的）：</p>
<pre class="brush:c">#define EI_NIDENT 16
typedef struct{
    unsigned char    e_ident[EI_NIDENT];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off   e_phoff;
    Elf32_Off   e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
}Elf32_Ehdr;</pre>
<p>ELF Header中各个字段说明如下：</p>
<table style="margin: 0pt auto; width: 500px;" border="1">
<tbody>
<tr>
<td>成员</td>
<td>说明</td>
</tr>
<tr>
<td>e_ident</td>
<td>ELF文件标识。</p>
<ul>
<li>前4个字节为ELF魔数，分别是0x7F、0&#215;45、0x4c、0&#215;46，第一个字节对应ASCII字符里面的DEL控制符，后面3个字节就是ELF这3个字母的ASCII码。</li>
<li>第5个字节标识ELF文件类别。0为无效文件，1为32位ELF文件，2为64位ELF文件。</li>
<li>第6个字节标识字节序。0为无效格式，1为小端格式，2为大端格式。</li>
<li>第7个字节为ELF主版本号，一般是1。</li>
<li>后面的9个字节ELF标准没有定义，一般填0。</li>
</ul>
</td>
</tr>
<tr>
<td>e_type</td>
<td>目标文件类型：</p>
<table style="width: 300px;" border="0">
<tbody>
<tr>
<td>名称</td>
<td>取值</td>
<td>含义</td>
</tr>
<tr>
<td>ET_NONE</td>
<td>0</td>
<td>未知目标文件格式</td>
</tr>
<tr>
<td>ET_REL</td>
<td>1</td>
<td>可重定位文件</td>
</tr>
<tr>
<td>ET_EXEC</td>
<td>2</td>
<td>可执行文件</td>
</tr>
<tr>
<td>ET_DYN</td>
<td>3</td>
<td>共享目标文件</td>
</tr>
<tr>
<td>ET_CORE</td>
<td>4</td>
<td>Core 文件(转储格式)</td>
</tr>
<tr>
<td>ET_LOPROC</td>
<td>0xff00</td>
<td>特定处理器文件</td>
</tr>
<tr>
<td>ET_HIPROC</td>
<td>0xffff</td>
<td>特定处理器文件</td>
</tr>
</tbody>
</table>
<p>ET_LOPROC 和 ET_HIPROC 之间的取值用来标识与处理器相关的文件格式。</td>
</tr>
<tr>
<td>e_machine</td>
<td>ELF文件的CPU平台属性：</p>
<table style="width: 300px;" border="0">
<tbody>
<tr>
<td>名称</td>
<td>取值</td>
<td>含义</td>
</tr>
<tr>
<td>EM_NONE</td>
<td>0</td>
<td>未指定</td>
</tr>
<tr>
<td>EM_M32</td>
<td>1</td>
<td>AT&amp;T WE 32100</td>
</tr>
<tr>
<td>EM_SPARC</td>
<td>2</td>
<td>SPARC</td>
</tr>
<tr>
<td>EM_386</td>
<td>3</td>
<td>Intel 80386</td>
</tr>
<tr>
<td>EM_68K</td>
<td>4</td>
<td>Motorola 68000</td>
</tr>
<tr>
<td>EM_68K</td>
<td>5</td>
<td>Motorola 68000</td>
</tr>
<tr>
<td>EM_860</td>
<td>7</td>
<td>Intel 80860</td>
</tr>
<tr>
<td>EM_MIPS</td>
<td>8</td>
<td>MIPS RS3000</td>
</tr>
</tbody>
</table>
<p>其它值都是保留的。特定处理器的ELF名称会使用机器名来进行区分。</td>
</tr>
<tr>
<td>e_version</td>
<td>ELF文件版本号，一般为常数1</p>
<table style="width: 300px;" border="0">
<tbody>
<tr>
<td>名称</td>
<td>取值</td>
<td>含义</td>
</tr>
<tr>
<td>EV_NONE</td>
<td>0</td>
<td>非法版本</td>
</tr>
<tr>
<td>EV＿CURRENT</td>
<td>1</td>
<td>当前版本</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>e_entry</td>
<td>程序入口的虚拟地址，操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位文件一般没有入口地址，则这个值为0。</td>
</tr>
<tr>
<td>e_phofff</td>
<td>程序头表（Program Header Table）在文件中的偏移量（以字节计算）。如果没有程序头表，可以为0。</td>
</tr>
<tr>
<td>e_shoff</td>
<td>段表在文件中的偏移量（以字节计算）</td>
</tr>
<tr>
<td>e_flags</td>
<td>ELF标志位，用来标识一些ELF文件平台相关的属性。相关常量的格式一般为EF_machine_flag，machine为平台，flag为标志。</td>
</tr>
<tr>
<td>e_ehsize</td>
<td>ELF文件头本身的大小。</td>
</tr>
<tr>
<td>e_phentsize</td>
<td>程序头表的表项大小。</td>
</tr>
<tr>
<td>e_phnum</td>
<td>程序头表的表项数目，可以为0。</td>
</tr>
<tr>
<td>e_shentsize</td>
<td>段表的表项大小。</td>
</tr>
<tr>
<td>e_shnum</td>
<td>段表的表项数目，可以为0。</td>
</tr>
<tr>
<td>e_shstrndx</td>
<td>段表字符串所在的段在段表中的下标。也就是段表字符串表的位置，可以从段表中用索引获取。</td>
</tr>
</tbody>
</table>
<p>本来很有冲动想将其它的一些段的格式说明也记录下来，但发现这样太费时间了，而且wp的编辑器有点让我抓狂。其实主要也就参考了《程序员的自我修养》这本书和《ELF文件格式分析》这份PDF文件。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/06/06/elf-file-format-details/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>可执行文件的装载</title>
		<link>http://blog.endlesscode.com/2010/06/05/load-elf/</link>
		<comments>http://blog.endlesscode.com/2010/06/05/load-elf/#comments</comments>
		<pubDate>Fri, 04 Jun 2010 17:04:16 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=799</guid>
		<description><![CDATA[对于32位平台下的4G的虚拟空间，在默认情况下，Linux操作系统将进程的虚拟地址空间划分为两部分，其中操作系统本身用去了一部分：从地址0xC0000000到0xFFFFFFFF，共1GB。剩下的从0&#215;00000000地址开始到0xBFFFFFFF共3GB的空间都是进程使用的。而对于Windows操作系统来说，它的进程虚拟地址空间划分是操作系统占用2GB。 覆盖装入（Overlay）和页映射（Paging）是两种很类型的动态装载方法，它们所采用的思想都差不多，原则上都是利用了程序的局部性原理。动态装入的思想是程序用到哪个模块，就将哪个模块装入内存，如果不用就暂时不装入，存放在磁盘中。 覆盖载入在没有发明虚拟存储之前使用比较广泛，现在已经几乎被淘汰了。覆盖载入的方法是把挖掘内存潜力的任务交给了程序员，即是程序员在编写代码的同时要自己手动对程序模块进行内存和磁盘的切换，使不需要的代码切换出磁盘，腾出空间给需要执行的代码块。 与覆盖载入的原理相似，页映射也不是一下子就把程序的所有数据和指令都载入内存，而是将内存和所有磁盘中的数据和指令按照“页（Page）”为单位划分分成若干个页，以后所有的装载和操作的单位就是页。以目前的情况，硬件规定的页大小有4096字节、8192字节、2MB、4MB等。其实这个是属于操作系统存储管理的一部分，内存页的切换会使用先进先出、最少使用等等算法。 可执行文件的装载 可执行文件的装载和进程的建立大概经历3个步骤： 首先是创建虚拟地址空间。一个虚拟空间是由一组页映射函数将虚拟空间的各个页映射至相应的物理空间，那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构。在i386的Linux下，创建虚拟地址空间实际上只是分配一个页目录（Page Directory）就可以了，甚至不设置页映射关系，这些映射关系等到后面程序发生页错误的时候再进行设置。 读取可执行文件头，并且建立虚拟空间与可执行文件的映射关系。上面一步的页映射关系函数是虚拟空间到物理内存的映射关系，这一步所做的是虚拟空间与可执行文件的映射关系。当程序执行发生页错误时，操作系统将从物理内存中分配一个物理页，然后将该“缺页”从磁盘中读取到内存中，再设置缺页的虚拟页和物理页的映射关系，这样程序才得以正常运行。当操作系统捕获到缺页错误时，它应知道程序当前所需要的页在可执行文件中的哪个位置。 将CPU指令寄存器设置成可执行文件入口，启动运行。这一步涉及了内核堆栈和用户堆栈的切换、CPU运行权限的切换等等。跳转到入口指令其实就是在ELF文件头中e_entry记录的可执行文件入口地址。 当ELF文件加载到进程后，进程的虚拟空间的分布 在ELF中文件，可以看到有很多段，包括.text、.init、.fini、.data、.bss等等。其实根据这些段的权限来组合，基本上可分为三种分类： 以代码段为代表的权限为可读可执行的段； 以数据段和BSS段为代码的权限为可读可写的段； 以只读数据段为代表的权限为只读的段。 而实际上，对于相同权限的段，可以把它们合并到一起当作一个段来进行映射。因此在ELF可执行文件中引入了一个概念叫做“Segment”，一个“Segment”包含一个或多个属性类似的“Section”，这种“Segment”就是在操作系统教材中讲述存储系统的时候经常接触到的（如段页式&#8230;）。“Segment”的概念实际上是从装载的角度重新划分了ELF的各个段。在将目标文件链接成可执行文件的时候，链接器会尽量把相同权限属性的段分配在同一个空间。而对于这些“Segment”，在ELF可执行文件中有一个专门的数据结构叫做程序头表（Program Header Table）用来保存“Segment”的信息，包括“Segment”的类型、“Segment”在文件中的偏移和第一个字节在进程虚拟地址空间的起始位置等等。 下面是用readelf看到的ELF可执行文件的程序头信息，还有&#8221;Segment&#8221;和&#8221;Section&#8221;映射的信息： Elf file type is EXEC (Executable file) Entry point 0x8048170 There are 6 program headers, starting at offset 52 Program Headers: &#8230; <a href="http://blog.endlesscode.com/2010/06/05/load-elf/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>对于32位平台下的4G的虚拟空间，在默认情况下，Linux操作系统将进程的虚拟地址空间划分为两部分，其中操作系统本身用去了一部分：从地址0xC0000000到0xFFFFFFFF，共1GB。剩下的从0&#215;00000000地址开始到0xBFFFFFFF共3GB的空间都是进程使用的。而对于Windows操作系统来说，它的进程虚拟地址空间划分是操作系统占用2GB。</p>
<p>覆盖装入（Overlay）和页映射（Paging）是两种很类型的动态装载方法，它们所采用的思想都差不多，原则上都是利用了程序的局部性原理。动态装入的思想是程序用到哪个模块，就将哪个模块装入内存，如果不用就暂时不装入，存放在磁盘中。</p>
<p>覆盖载入在没有发明虚拟存储之前使用比较广泛，现在已经几乎被淘汰了。覆盖载入的方法是把挖掘内存潜力的任务交给了程序员，即是程序员在编写代码的同时要自己手动对程序模块进行内存和磁盘的切换，使不需要的代码切换出磁盘，腾出空间给需要执行的代码块。</p>
<p>与覆盖载入的原理相似，页映射也不是一下子就把程序的所有数据和指令都载入内存，而是将内存和所有磁盘中的数据和指令按照“页（Page）”为单位划分分成若干个页，以后所有的装载和操作的单位就是页。以目前的情况，硬件规定的页大小有4096字节、8192字节、2MB、4MB等。其实这个是属于操作系统存储管理的一部分，内存页的切换会使用先进先出、最少使用等等算法。</p>
<h4>可执行文件的装载</h4>
<p>可执行文件的装载和进程的建立大概经历3个步骤：</p>
<ul>
<li>首先是创建虚拟地址空间。一个虚拟空间是由一组页映射函数将虚拟空间的各个页映射至相应的物理空间，那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构。在i386的Linux下，创建虚拟地址空间实际上只是分配一个页目录（Page Directory）就可以了，甚至不设置页映射关系，这些映射关系等到后面程序发生页错误的时候再进行设置。</li>
<li>读取可执行文件头，并且建立虚拟空间与可执行文件的映射关系。上面一步的页映射关系函数是虚拟空间到物理内存的映射关系，这一步所做的是虚拟空间与可执行文件的映射关系。当程序执行发生页错误时，操作系统将从物理内存中分配一个物理页，然后将该“缺页”从磁盘中读取到内存中，再设置缺页的虚拟页和物理页的映射关系，这样程序才得以正常运行。当操作系统捕获到缺页错误时，它应知道程序当前所需要的页在可执行文件中的哪个位置。</li>
<li>将CPU指令寄存器设置成可执行文件入口，启动运行。这一步涉及了内核堆栈和用户堆栈的切换、CPU运行权限的切换等等。跳转到入口指令其实就是在ELF文件头中e_entry记录的可执行文件入口地址。</li>
</ul>
<h4><span id="more-799"></span></h4>
<h4>当ELF文件加载到进程后，进程的虚拟空间的分布</h4>
<p>在ELF中文件，可以看到有很多段，包括.text、.init、.fini、.data、.bss等等。其实根据这些段的权限来组合，基本上可分为三种分类：</p>
<ul>
<li>以代码段为代表的权限为可读可执行的段；</li>
<li>以数据段和BSS段为代码的权限为可读可写的段；</li>
<li>以只读数据段为代表的权限为只读的段。</li>
</ul>
<p>而实际上，对于相同权限的段，可以把它们合并到一起当作一个段来进行映射。因此在ELF可执行文件中引入了一个概念叫做“Segment”，一个“Segment”包含一个或多个属性类似的“Section”，这种“Segment”就是在操作系统教材中讲述存储系统的时候经常接触到的（如段页式&#8230;）。“Segment”的概念实际上是从装载的角度重新划分了ELF的各个段。在将目标文件链接成可执行文件的时候，链接器会尽量把相同权限属性的段分配在同一个空间。而对于这些“Segment”，在ELF可执行文件中有一个专门的数据结构叫做程序头表（Program Header Table）用来保存“Segment”的信息，包括“Segment”的类型、“Segment”在文件中的偏移和第一个字节在进程虚拟地址空间的起始位置等等。</p>
<p>下面是用readelf看到的ELF可执行文件的程序头信息，还有&#8221;Segment&#8221;和&#8221;Section&#8221;映射的信息：</p>
<pre class="brush:plain">Elf file type is EXEC (Executable file)
Entry point 0x8048170
There are 6 program headers, starting at offset 52

Program Headers:
  Type                   Offset       VirtAddr         PhysAddr     FileSiz     MemSiz  Flg   Align
  LOAD                 0x000000 0x08048000 0x08048000 0x7c7e9  0x7c7e9 R E  0x1000
  LOAD                 0x07cf98 0x080c5f98   0x080c5f98  0x00788 0x022bc RW  0x1000
  NOTE                 0x0000f4 0x080480f4   0x080480f4  0x00044 0x00044 R     0x4
  TLS                    0x07cf98  0x080c5f98   0x080c5f98  0x00010 0x00028 R     0x4
  GNU_STACK      0x000000 0x00000000  0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x07cf98  0x080c5f98   0x080c5f98  0x00068 0x00068 R     0x1

 Section to Segment mapping:
  Segment Sections...
   00     .note.ABI-tag .note.gnu.build-id .init .text __libc_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .eh_frame .gcc_except_table
   01     .tdata .ctors .dtors .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
   02     .note.ABI-tag .note.gnu.build-id
   03     .tdata .tbss
   04
   05     .tdata .ctors .dtors .jcr .data.rel.ro .got .got.plt</pre>
<p>在操作系统里面，除了给代码段和数据段等分配VMA之外，还会分配栈和堆的空间。因此，在操作系统里面可以根据相同的权限属性划分成几个VMA的区域：</p>
<ul>
<li>代码VMA，权限只读、可执行；有映像文件。</li>
<li>数据VMA，权限可读写、可执行；有映像文件。</li>
<li>堆VMA，权限可读写、可执行；无映像文件，匿名，可向上扩展。</li>
<li>栈VMA，权限可读写、不可执行；无映像文件，匿名，可向下扩展。</li>
</ul>
<h4>进程启动时输入的参数</h4>
<p>我们知道进程刚开始启动的时候，须知道一些进程运行的环境，最基本的就是系统环境变量和进程的运行参数。很常见的一种做法是操作系统在进程启动前将这些信息保存到进程的虚拟空间的栈中（也就是VMA中的Stack VMA）。假设系统中有两个环境变量如下：</p>
<pre class="brush:bash">HOME=/home/user
PATH=/usr/bin</pre>
<p>运行以下命令：</p>
<pre class="brush:bash">prog 123</pre>
<p>此时，栈顶寄存器esp指向的位置是初始化以后堆栈的顶部，最前面的4个字节表示命令行参数的数量，我们的例子里面是两个，即“prog”和“123”，紧接着的就是分别指向这两个参数字符串的指针；后面跟一个0；接着是两个指向环境变量字符串的指针，它们分别指向字符串“HOME=/home/user”和“PATH=/usr/bin”；后面紧跟一个0表示结束。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/06/05/load-elf/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>静态链接</title>
		<link>http://blog.endlesscode.com/2010/06/03/static-link/</link>
		<comments>http://blog.endlesscode.com/2010/06/03/static-link/#comments</comments>
		<pubDate>Wed, 02 Jun 2010 19:27:44 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=705</guid>
		<description><![CDATA[链接有哪些过程？ 假如有2个源文件如下，这两个源文件生成的目标文件链接成可执行文件后，涉及到空间和地址的分配、符号解析和重地位等等过程。 /* a.c */ extern int shared; int main() { int a = 100; swap( &#38;a, &#38;shared); } /* b.c */ int shared = 1; void swap( int* a, int* b) { *a ^= *b ^= *a ^= *b; &#8230; <a href="http://blog.endlesscode.com/2010/06/03/static-link/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<h4>链接有哪些过程？</h4>
<p>假如有2个源文件如下，这两个源文件生成的目标文件链接成可执行文件后，涉及到空间和地址的分配、符号解析和重地位等等过程。</p>
<pre class="brush:c++">/* a.c */
extern int shared;

int main()
{
    int a = 100;
    swap( &amp;a, &amp;shared);
}</pre>
<pre class="brush:c++">/* b.c */
int shared = 1;

void swap( int* a, int* b)
{
    *a ^= *b ^= *a ^= *b;
}</pre>
<p>在a.c中引用到了b.c中的变量&#8221;shared&#8221;和函数&#8221;swap&#8221;。使用gcc编译成目标文件a.o和b.o之后，接着就需要将两个目标文件链接成一个可执行文件。</p>
<pre class="brush:bash">gcc -c a.c b.c</pre>
<p>一般来说，链接过程分为两步：<span id="more-705"></span></p>
<ol>
<li>空间和地址分配。扫描所有的输入目标文件，获得它们的各个段的长度、属性和位置，并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来，统一放到一个全局符号表。这一步中，链接器将能够获得所有目标文件的段长度，并且将它们合并，计算出输出文件 中各个段合并后的长度与位置，并建立映射关系。</li>
<li>符号解析与重定位。使用上面第一步中收集到的所有信息，读取输入文件中段的数据、重定位信息，并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心。</li>
</ol>
<p>在空间和地址的分配上比较实际的方法是将相同性质的段合并到一起，比如将所有输入文件的&#8221;.text&#8221;合并到输出文件的&#8221;.text&#8221;段，接着是&#8221;.data&#8221;段、&#8221;.bss&#8221;段等。在目标文件链接之前，目标文件中的所有段的VMA(Virtual Memory Address)都是0，因为虚拟空间还没有被分配，所以它们默认都为0。等到链接之后，可执行文件中的各个段都会被分配到了相应的虚拟地址。由于操作系统的进程虚拟地址空间的分配规则不同，因此并不是所有的虚拟地址都是从0地址开始分配的，在Linux下，ELF可执行文件默认从地址0&#215;08048000开始分配的。在第一步的扫描和空间分配阶段，链接器按照前面介绍的空间分配方法进行分配，这时候输入文件中的各个段在链接后的虚拟地址就已经确定了。</p>
<p>a.o的段属性，&#8221;.text&#8221;、&#8221;.data&#8221;、&#8221;.bss&#8221;的VMA全为0：</p>
<pre class="brush:bash">a.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000034  00000000  00000000  00000034  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  00000000  00000000  00000068  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  00000068  2**2
                  ALLOC</pre>
<p>b.o的段属性，&#8221;.text&#8221;、&#8221;.data&#8221;、&#8221;.bss&#8221;的VMA也全为0：</p>
<pre class="brush:bash">b.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000003e  00000000  00000000  00000034  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  00000000  00000000  00000074  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  00000078  2**2
                  ALLOC</pre>
<p>ab的段属性，将a.o和b.o链接后可以看到&#8221;.text&#8221;的虚拟地址是从0&#215;08048094开始的，而&#8221;.data&#8221;的虚拟地址是从0&#215;08049108开始的：</p>
<pre class="brush:bash">ab:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000072  08048094  08048094  00000094  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  08049108  08049108  00000108  2**2
                  CONTENTS, ALLOC, LOAD, DATA</pre>
<h4>链接器怎么知道哪些指令是要被调整的？</h4>
<p>事实上在ELF文件中，有一个叫重定位表（Relocation Table）的结构专门用来保存这些与重定位相关的信息（编译的时候生成）。重定位表往往就是ELF文件中的一个段，比如代码段&#8221;.text&#8221;如有要被重定位的地方，那么会有一个相对应叫&#8221;.rel.text&#8221;的段保存了代码段的重定位表；如果数据段&#8221;.data&#8221;有要被重定位的地方，就会有一个相对应叫&#8221;.rel.data&#8221;的段保存了数据段的重定位表。</p>
<p>重定位表也是一个有固定元素结构的数组。数组中的元素保存了每个重定位项 的入口偏移(r_offset)、入口的类型和符号(r_info)。重定位的入口偏移(r_offset)是指该重定位入口所要修正的位置的第一个字节相对于段起始的偏移。至于r_info，这个成员的低8位表示重定位入口的类型，高24位表示重定位入口的符号在符号表中的下标。</p>
<p>其实重定位过程也伴随着符号的解析过程，每个目标文件都可能定义一些符号，也可能引用到定义在其他目标文件的符号。重定位的过程中，每个重定位的入口都是对一个符号的引用，那么当链接器须要对某个符号的引用进行重定位时，它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表，找到相应的符号表后进行重定位。</p>
<h4>重定位的具体过程是怎样？针对哪些指令进行重定位？</h4>
<p>对于32位x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有两种：绝对近址32位寻址和相对近址32位寻址。每个被修正的位置的长度都为32位（修正的都是地址），即4个字节。重定位入口(r_info)的低8位指明了重定位入口的类型，1表示绝对寻址修正，2表示相对寻址修正。</p>
<p>首先需要明确的是，程序代码里面使用的是虚拟地址，在空间分配之前，所有的全局变量和函数地址都为0，等到空间分配之后，各个函数和变量才会确定自己在虚拟地址空间中的位置。对于近址相对位移调用指令（如call），后面4个字节就是被调用函数的相对于调用指令的下一条指令的偏移量。在没有重定位之前，相对偏移被置为0xFFFFFFFC（小端），它是常量&#8221;-4&#8243;的补码形式。</p>
<p>把a.o反汇编之后：</p>
<pre class="brush:bash">a.o:     file format elf32-i386

Disassembly of section .text:

00000000 &lt;main&gt;:
 0:    8d 4c 24 04              lea    0x4(%esp),%ecx
 4:    83 e4 f0                 and    $0xfffffff0,%esp
 7:    ff 71 fc                 pushl  -0x4(%ecx)
 a:    55                       push   %ebp
 b:    89 e5                    mov    %esp,%ebp
 d:    51                       push   %ecx
 e:    83 ec 24                 sub    $0x24,%esp
 11:    c7 45 f8 64 00 00 00     movl   $0x64,-0x8(%ebp)
 18:    c7 44 24 04 00 00 00     movl   $0x0,0x4(%esp)
 1f:    00
 20:    8d 45 f8                 lea    -0x8(%ebp),%eax
 23:    89 04 24                 mov    %eax,(%esp)
 26:    e8 fc ff ff ff           call   27 &lt;main+0x27&gt;
 2b:    83 c4 24                 add    $0x24,%esp
 2e:    59                       pop    %ecx
 2f:    5d                       pop    %ebp
 30:    8d 61 fc                 lea    -0x4(%ecx),%esp
 33:    c3                       ret</pre>
<p>偏移为0&#215;18的行中，&#8221;c7 44 24 04 00 00 00 00&#8243;，前4个字节为mov的指令码，后面4个字节为&#8221;shared&#8221;变量的地址，在空间和地址分配之前值为0。偏移为0&#215;26的行中，&#8221;e8 fc ff ff ff&#8221;，&#8221;e8&#8243;为call的指令码，&#8221;fc ff ff ff&#8221;正是swap的地址，在空间和地址分配之前为0xFFFFFFFC（小端）。</p>
<p>重定位的类型：</p>
<ol>
<li>绝对寻址修正（类型=1）：S+A</li>
<li>相对寻址修正（类型=2）：S+A-P</li>
</ol>
<p>其中，A是保存被修正的值，即链接前该变量或者函数的值。S是符号的实际地址，即链接后的虚拟地址，由r_info的高24位指定的符号的实际地址。P是被修正的位置，即是相对于段开始的偏移量或者虚拟地址。</p>
<p>将链接后的ab反汇编之后，再根据上面的计算方式简单演示一下：</p>
<pre class="brush:bash">ab:     file format elf32-i386

Disassembly of section .text:

08048094 &lt;main&gt;:
 8048094:    8d 4c 24 04              lea    0x4(%esp),%ecx
 8048098:    83 e4 f0                 and    $0xfffffff0,%esp
 804809b:    ff 71 fc                 pushl  -0x4(%ecx)
 804809e:    55                       push   %ebp
 804809f:    89 e5                    mov    %esp,%ebp
 80480a1:    51                       push   %ecx
 80480a2:    83 ec 24                 sub    $0x24,%esp
 80480a5:    c7 45 f8 64 00 00 00     movl   $0x64,-0x8(%ebp)
 80480ac:    c7 44 24 04 08 91 04     movl   $0x8049108,0x4(%esp)
 80480b3:    08
 80480b4:    8d 45 f8                 lea    -0x8(%ebp),%eax
 80480b7:    89 04 24                 mov    %eax,(%esp)
 80480ba:    e8 09 00 00 00           call   80480c8 &lt;swap&gt;
 80480bf:    83 c4 24                 add    $0x24,%esp
 80480c2:    59                       pop    %ecx
 80480c3:    5d                       pop    %ebp
 80480c4:    8d 61 fc                 lea    -0x4(%ecx),%esp
 80480c7:    c3                       ret    

080480c8 &lt;swap&gt;:
 80480c8:    55                       push   %ebp
 80480c9:    89 e5                    mov    %esp,%ebp
 80480cb:    53                       push   %ebx
 80480cc:    8b 45 08                 mov    0x8(%ebp),%eax
 80480cf:    8b 18                    mov    (%eax),%ebx
 80480d1:    8b 45 0c                 mov    0xc(%ebp),%eax
 80480d4:    8b 08                    mov    (%eax),%ecx
 80480d6:    8b 45 08                 mov    0x8(%ebp),%eax
 80480d9:    8b 10                    mov    (%eax),%edx
 80480db:    8b 45 0c                 mov    0xc(%ebp),%eax
 80480de:    8b 00                    mov    (%eax),%eax
 80480e0:    31 c2                    xor    %eax,%edx
 80480e2:    8b 45 08                 mov    0x8(%ebp),%eax
 80480e5:    89 10                    mov    %edx,(%eax)
 80480e7:    8b 45 08                 mov    0x8(%ebp),%eax
 80480ea:    8b 00                    mov    (%eax),%eax
 80480ec:    89 ca                    mov    %ecx,%edx
 80480ee:    31 c2                    xor    %eax,%edx
 80480f0:    8b 45 0c                 mov    0xc(%ebp),%eax
 80480f3:    89 10                    mov    %edx,(%eax)
 80480f5:    8b 45 0c                 mov    0xc(%ebp),%eax
 80480f8:    8b 00                    mov    (%eax),%eax
 80480fa:    89 da                    mov    %ebx,%edx
 80480fc:    31 c2                    xor    %eax,%edx
 80480fe:    8b 45 08                 mov    0x8(%ebp),%eax
 8048101:    89 10                    mov    %edx,(%eax)
 8048103:    5b                       pop    %ebx
 8048104:    5d                       pop    %ebp
 8048105:    c3                       ret

Disassembly of section .data:

08049108 &lt;shared&gt;:
 8049108:    01 00                    add    %eax,(%eax)</pre>
<p>首先，从&#8221;.data&#8221;段可以看到shared的实际地址为&#8221;0&#215;08049108&#8243;，值为1，而shared的修正方式是绝对地址修正，因此，对于原偏移为0x1c的这个重定位入口，它的修正后的结果应该是S（0&#215;08049108）+A（0&#215;00000000）= 0&#215;08049108。看回到偏移为18行，已经变成&#8221;c7 44 24 04 08 91 04 08&#8243;，后面的4个字节正是&#8221;0&#215;08049108&#8243;，结果匹配。</p>
<p>而对于原偏移0&#215;27的swap(call swap)的修正则是采用相对寻址修正，它的修正结果应该是S（0x080480c8）+A（0xfffffffc）-P（0x80480bb）= 0&#215;00000009，与偏移0&#215;27的&#8221;09 00 00 00&#8243;匹配。其实相对寻址修正就是计算地址差，可以看到从反汇编的代码中看出，链接之后swap函数的地址为&#8221;0x080480c8&#8243;，而&#8221;call swap&#8221;的下一条指令的虚拟地址为&#8221;0x080480bf&#8221;，因此&#8221;0x080480bf&#8221; + &#8220;0&#215;00000009&#8243; = &#8220;0x080480c8&#8243;。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/06/03/static-link/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ELF文件结构简述</title>
		<link>http://blog.endlesscode.com/2010/05/31/elf-file-structure-short-intro/</link>
		<comments>http://blog.endlesscode.com/2010/05/31/elf-file-structure-short-intro/#comments</comments>
		<pubDate>Mon, 31 May 2010 18:34:56 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=674</guid>
		<description><![CDATA[现在PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format)，它们都是COFF(Common File Format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o)，它跟可执行文件的内容与结构很相似，所以一般跟可执行文件格式一起采用一种格式存储。 Linux的.o/Windows的.obj、/bin/bash或Windows的exe、Linux的.so/Windows的.dll分别是什么文件？ Linux的.o和Windows的.obj称为可重定位文件(Relocatable File)，这类文件包含了代码和数据，可以被用来链接成可执行文件或共享目标文件，静态链接库也可以归为这一类。 /bin/bash和Windows的.exe是可以直接执行的程序，它的代表就是ELF可执行文件，它们一般是没有扩展名的。 Linux的.so和Windows的.dll称为共享目标文件，这种文件也包含了代码和数据，可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重位文件和共享目标文件链接、产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合，作为进程映像的一部分来执行。 ELF目标文件的格式是怎样？ ELF文件除了包括了上述图示中的机器指令代码、数据，还包括了链接时所需要的一些信息，比如符号表、调试信息、字符串等。这些信息按不同的属性，以“段“(Segment)的形式存储。程序源代码编译后的机器指令经常放在代码段(Code Section)里，代码段常见的名字有&#8221;.code&#8221;或&#8221;.text&#8221;；全局变量和局部静态变量经常放在数据段(Data Section)，数据段一句名字叫为&#8221;.data&#8221;。而ELF文件的开头是一个“文件头”，它描述了整个文件的文件属性，包括文件是否可执行、是静态链接还是地动态链接及入口地址（如果是可执行文件）、目标硬件、目标操作系统等信息，文件头还包括一个段表的位置信息，段表在ELF文件中是一个描述文件中各个段的数组，包括各个段在文件的偏移位置，段的属性，段的名称等等。 程序源代码被编译以后主要分成两种段：程序指令和程序数据。代码段(.text)属于程序指令，而数据段(.data)和.bss段属于程序数据。 一般C语言的编译后执行语句都编译成机器代码，保存在.text段；已初始化的全局变量和局部静态变量都保存在.data段；未初始化的全局变量和局部静 态变量一般放在.bss段，这些未初始化的全局变量和局部静态变量默认值都为0，放在.data段显示没有必要，因此放在了.bss段。所以.bss段只 是为未初始化的全局变量和局部静态变量预留位置，它并没有内容，所以它在文件中也不占据空间。 一般程序被装载后，数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读可写的，而指令区域对于进程来是只读的，所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。 常用的段名 说明 .rodata1 Read Only Data，这种段里存放的是只读数据，比如字符串常量、全局const变量。跟&#8221;.rodata&#8221;一样 .comment 存放的是编译器版本信息 .debug 调试信息 .dynamic 动态链接信息 .hash 符号哈希表 .line 调试时的行号表 .note 额外的编译器信息。比如程序的公司名、发布版本号等 .strtab String &#8230; <a href="http://blog.endlesscode.com/2010/05/31/elf-file-structure-short-intro/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>现在PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format)，它们都是COFF(Common File Format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o)，它跟可执行文件的内容与结构很相似，所以一般跟可执行文件格式一起采用一种格式存储。</p>
<h4>Linux的.o/Windows的.obj、/bin/bash或Windows的exe、Linux的.so/Windows的.dll分别是什么文件？</h4>
<ol>
<li>Linux的.o和Windows的.obj称为<strong>可重定位文件</strong>(Relocatable File)，这类文件包含了代码和数据，可以被用来链接成<strong>可执行文件</strong>或<strong>共享目标文件</strong>，<strong>静态链接库</strong>也可以归为这一类。</li>
<li>/bin/bash和Windows的.exe是可以直接执行的程序，它的代表就是<strong>ELF可执行文件</strong>，它们一般是没有扩展名的。</li>
<li>Linux的.so和Windows的.dll称为<strong>共享目标文件</strong>，这种文件也包含了代码和数据，可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重位文件和共享目标文件链接、产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合，作为进程映像的一部分来执行。</li>
</ol>
<h4>ELF目标文件的格式是怎样？</h4>
<p><img class="aligncenter size-full wp-image-680" title="elf struct" src="http://blog.endlesscode.com/wp-content/uploads/2010/05/elf-struct1.jpg" alt="elf struct" width="500" height="344" /><span id="more-674"></span>ELF文件除了包括了上述图示中的机器指令代码、数据，还包括了链接时所需要的一些信息，比如符号表、调试信息、字符串等。这些信息按不同的属性，以“段“(Segment)的形式存储。程序源代码编译后的机器指令经常放在代码段(Code Section)里，代码段常见的名字有&#8221;.code&#8221;或&#8221;.text&#8221;；全局变量和局部静态变量经常放在数据段(Data Section)，数据段一句名字叫为&#8221;.data&#8221;。而ELF文件的开头是一个“文件头”，它描述了整个文件的文件属性，包括文件是否可执行、是静态链接还是地动态链接及入口地址（如果是可执行文件）、目标硬件、目标操作系统等信息，文件头还包括一个段表的位置信息，段表在ELF文件中是一个描述文件中各个段的数组，包括各个段在文件的偏移位置，段的属性，段的名称等等。</p>
<p>程序源代码被编译以后主要分成两种段：程序指令和程序数据。代码段(.text)属于程序指令，而数据段(.data)和.bss段属于程序数据。 一般C语言的编译后执行语句都编译成机器代码，保存在.text段；已初始化的全局变量和局部静态变量都保存在.data段；未初始化的全局变量和局部静 态变量一般放在.bss段，这些未初始化的全局变量和局部静态变量默认值都为0，放在.data段显示没有必要，因此放在了.bss段。所以.bss段只 是为未初始化的全局变量和局部静态变量预留位置，它并没有内容，所以它在文件中也不占据空间。</p>
<p>一般程序被装载后，数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读可写的，而指令区域对于进程来是只读的，所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。</p>
<table border="1" width="80%">
<tbody>
<tr>
<td>常用的段名</td>
<td>说明</td>
</tr>
<tr>
<td>.rodata1</td>
<td>Read Only Data，这种段里存放的是只读数据，比如字符串常量、全局const变量。跟&#8221;.rodata&#8221;一样</td>
</tr>
<tr>
<td>.comment</td>
<td>存放的是编译器版本信息</td>
</tr>
<tr>
<td>.debug</td>
<td>调试信息</td>
</tr>
<tr>
<td>.dynamic</td>
<td>动态链接信息</td>
</tr>
<tr>
<td>.hash</td>
<td>符号哈希表</td>
</tr>
<tr>
<td>.line</td>
<td>调试时的行号表</td>
</tr>
<tr>
<td>.note</td>
<td>额外的编译器信息。比如程序的公司名、发布版本号等</td>
</tr>
<tr>
<td>.strtab</td>
<td>String Table.字符串表</td>
</tr>
<tr>
<td>.symtab</td>
<td>Symbol Table.符号表</td>
</tr>
<tr>
<td>.shstrtab</td>
<td>Section String Table.段名表</td>
</tr>
<tr>
<td>.plt<br />
.got</td>
<td>动态链接的跳转表和全局入口表</td>
</tr>
<tr>
<td>.init<br />
.fini</td>
<td>程序初始化与终结代码段</td>
</tr>
</tbody>
</table>
<p>其中，&#8221;.rodata&#8221;段存放的是只读数据，一般是程序里面的只读变量（如const修饰的变量）和字符串常量。单独设立&#8221;.rodata&#8221;段有很多好 处，不光是在语义上支持了C++的const关键字，而且操作系统在加载的时候可以将&#8221;.rodata&#8221;段的属性映射成只读，这样对于这个段的任何修改操 作都会作为非法操作处理，保证程序的安全性。当然，有些编译器也会把字符串放到&#8221;.data&#8221;段，而不会单独放在&#8221;.rodata&#8221;段。</p>
<h4>如C中的printf(&#8220;%d\nHello World!&#8221;)中的字符&#8221;%d\nHello World!&#8221;是存储在哪里的？</h4>
<p>ELF文件中用到了很多字符串，比如段名、变量名等。因为字符串的长度往往是不定的，所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表，然后使用字符串在表中的偏移来引用字符串。如下图所示：</p>
<p><img class="aligncenter size-full wp-image-684" title="strtab" src="http://blog.endlesscode.com/wp-content/uploads/2010/05/strtab.jpg" alt="strtab" width="500" height="253" /></p>
<p>通过这种方法，在ELF文件中引用字符串只须给出一个数字下标即可，不用考虑字符串长度的问题。一般字符串表在ELF文件中也以段的形式保存，常见的段名为&#8221;<strong>.strtab</strong>&#8220;或&#8221;<strong>.shstrtab</strong>&#8220;。这两个字符串分别为<strong>字符串表</strong>(<strong>String Table</strong>)和<strong>段表字符串表</strong>(<strong>Section Header String Table</strong>)。顾名思义，字符串表用来保存普通的字符串，比如符号的名字；段表字符串表用来保存段表中用到的字符串，最常见的就是段名。</p>
<h4>段表是结构是怎样？存储了哪些信息？</h4>
<p>段表的结构其实就是一个数组，数组的元素是固定的结构，每个元素包括了各个段的信息，比如每个段的段名、段的长度、在文件中的偏移、读写权限、段的链接信息以及段的其他属性。其中描述段名的只是一个偏移，是段名字符串在&#8221;.shstrtab&#8221;段中的偏移。那么如何根据ELF文件头和段表来定位文件中的一个段呢？从ELF文件头中获取<strong>段表在文件中的偏移</strong>以及<strong>段表的大小</strong>，而从获取到了文件中的段表，再分析段表数组中的每个元素，得知段的偏移、段的长度和段的类型，就可以准确地定位一个段了。</p>
<h4>字符串表和符号表有什么不同？</h4>
<p>其实是完全不同的两个表，但是两个表又有关联。简单地说，字符串表就是记录ELF文件中的字符串常量，变量名等等。而符号表的存在意义是体现在多个目标文件进行链接的时候，在链接中，目标文件之间相互拼合实际上是目标文件之间对地址的引用，即对函数和变量的地址的引用，而函数和变量可以统称为<strong>符号</strong>(<strong>Symbol</strong>)，函数名或变量名就是<strong>符号名</strong>(<strong>Symbol Name</strong>)。我们可以将符号看作是是链接中的粘合剂，整个链接过程就是基于符号才能够正确完成。在符号表&#8221;<strong>.symtab</strong>&#8220;中，其也是像段表的结构一样，是一个数组，每个数组元素是一个固定的结构来保存符号的相关信息，比如符号名（不是字符串，而是该符号名在字符串表的下标）、符号对应的值（可能是段中的偏移，也可能是符号的虚拟地址）、符号大小（数据类型的大小）等等。符号表中记录的一般是全局符号，比如全局变量、全局函数等等。</p>
<h4>Visual Studio里面提示的Undefined Symbol,为什么是类似(?NewL@CHtmlControl@@SAPAV1@PAVCCoeControl@@@Z)这么诡异的symbol？</h4>
<p>程序里面肯定是没有定义过&#8221;?NewL@CHtmlControl@@SAPAV1@PAVCCoeControl@@@Z&#8221;这么诡异的变量名的，但为什么变量名会变成这么诡异的？这是因为符号修饰的原因。符号修饰很大程度上是为了防止符号名冲突。比如复杂的C++拥有类、继承、虚拟机制、重载、名称空间等这些特性，如果两个函数func(int)和func(float)不进行符号修饰而直接存储变量名的话，然后只是单单根据函数名来判别的话，那么符号表就会出现2个一样的符号了。根据函数签名来修饰两个重载函数就不会出现这样的符号二义性的问题了。因为符号修饰的规则是各编译器厂商都不一样，因此再诡异也是没有什么出奇的。</p>
<h4>如果两个目标文件定义了相同的全局变量？</h4>
<p>在编辑过程中可能会遇到一种情况叫符号重复定义。这涉及到强符号（Strong Symbol）和弱符号（Weak Symbol）的定义。对于C/C++语言来说，编译默认函数和初始化了的全局变量为强符号，未初始化的全局变量为弱符号。我们也可以通过GCC的&#8221;__attribute__((weak))&#8221;来定义任何一个强符号为弱符号，而用&#8221;extern int ext&#8221;这样的方式定义并非弱符号也非弱符号，因为&#8221;ext&#8221;只是一个外部变量的引用。针对强弱符号的概念，链接器一般会按如下规则选择全局符号：</p>
<ul>
<li>不允许强符号被多次定义，否则链接器会报符号重复定义的错误。</li>
<li>如果一个符号在某个目标文件中是强符号，在其他文件中都是弱符号，那么选择强符号。</li>
<li>如果一个符号在所有目标文件中都是弱符号，那么选择其中占用空间最大的一个。</li>
</ul>
<h4>预编译过程主要做了哪些处理？</h4>
<ol>
<li>将所有的&#8221;#define&#8221;删除，并且展开所有的宏定义。</li>
<li>处理所有条件预编译指令，比如&#8221;#if&#8221;, &#8220;#ifdef&#8221;, &#8220;#elif&#8221;, &#8220;#else&#8221;, &#8220;endif&#8221;。</li>
<li>处理&#8221;#include&#8221;预编译指令，将被包含的文件插入到该预编译指令的位置。注意，这个过程是递归进行的，也就是说被包含的文件可能还包含其他文件。</li>
<li>删除所有的注释&#8221;//&#8221;和&#8221;/**/&#8221;。</li>
<li>添加行号和文件名标识，比如#2 &#8220;hello.c&#8221; 2，以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。</li>
<li>保留所有的#pragma编译器指令，因为编译器须要使用它们。</li>
</ol>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/05/31/elf-file-structure-short-intro/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>实模式切换到保护模式</title>
		<link>http://blog.endlesscode.com/2010/03/27/rmode-to-pmode/</link>
		<comments>http://blog.endlesscode.com/2010/03/27/rmode-to-pmode/#comments</comments>
		<pubDate>Sat, 27 Mar 2010 15:37:50 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=534</guid>
		<description><![CDATA[代码是从于渊的书上抄过来的，不过书的注释简单得实在不行，加上汇编都差不多给回学校了，看了好几遍才把这些代码看透。看到虚拟机的屏幕上输出个&#8221;P&#8221;感觉不错 :) 。 从实模式到保护模式一般的过程为： 建立起合适的全局描述符表。 用lgdt加载GDTR，使GDTR指向GDT。 打开A20地址线。PC及其兼容机的第20根地址线较特殊，计算机系统中一般安排一个“门”控制该地址线是否有效。为了访问地址在1M以上的存储单元，应先打开控制地址线A20的“门”。这种设置与实方式下只使用最的1M字节存储空间有关，与处理器是否工作在实方式和保护方式无关，即使在关闭地址线A20时，也可进行保护方式。 置cr0的PE位。PE位为1表示运行在保护模式，为0表示运行在实方式。 跳转，进入保护模式。 简单的切换代码如下（这个asm的着色是自己加上去的，着色得有点丑陋:)）： %macro Descriptor 3 dw %2 &#38; 0FFFFh ;; segment limit 1 dw %1 &#38; 0FFFFh ;; segment base 1 db (%1 &#62;&#62; 16) &#38; 0FFh ;; segment base 2 dw &#8230; <a href="http://blog.endlesscode.com/2010/03/27/rmode-to-pmode/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>代码是从于渊的书上抄过来的，不过书的注释简单得实在不行，加上汇编都差不多给回学校了，看了好几遍才把这些代码看透。看到虚拟机的屏幕上输出个&#8221;P&#8221;感觉不错 :) 。</p>
<p>从实模式到保护模式一般的过程为：</p>
<ul>
<li>建立起合适的全局描述符表。</li>
<li>用lgdt加载GDTR，使GDTR指向GDT。</li>
<li>打开A20地址线。PC及其兼容机的第20根地址线较特殊，计算机系统中一般安排一个“门”控制该地址线是否有效。为了访问地址在1M以上的存储单元，应先打开控制地址线A20的“门”。这种设置与实方式下只使用最的1M字节存储空间有关，与处理器是否工作在实方式和保护方式无关，即使在关闭地址线A20时，也可进行保护方式。</li>
<li>置cr0的PE位。PE位为1表示运行在保护模式，为0表示运行在实方式。</li>
<li>跳转，进入保护模式。<span id="more-534"></span></li>
</ul>
<p>简单的切换代码如下（这个asm的着色是自己加上去的，着色得有点丑陋:)）：</p>
<pre class="brush:asm">%macro Descriptor 3
    dw      %2 &amp; 0FFFFh                                  ;; segment limit 1
    dw      %1 &amp; 0FFFFh                                  ;; segment base 1
    db      (%1 &gt;&gt; 16) &amp; 0FFh                            ;; segment base 2
    dw      ((%2 &gt;&gt; 8) &amp; 0F00h) | (%3 &amp; 0F0FFh)          ;; properties1 + segment limit 2 + properties2
    db      (%1 &gt;&gt; 24) &amp; 0FFh                            ;; segment base 3
%endmacro

;; descriptor type
DA_32       equ     4000h   ;; 32 bits segment，D位设为1

;; descriptor type of data segment
DA_DRW      equ     92h     ;; accessed &amp; readable
DA_C        equ     98h     ;; accessed &amp; code segment

org     07c00h               ;;将这段程序加载到内存偏移地址0x7c00处 ，即引导代码的开始处
jmp     LABEL_BEGIN

[SECTION .gdt]
;; GDT 全局描述符表
                                    ;; base  limit   attr
LABEL_GDT:              Descriptor      0,      0,      0                           ;; void descriptor, gdt表第一个元素需为0
LABEL_DESC_CODE32:      Descriptor      0,      SegCode32Len - 1,     DA_C + DA_32  ;; unconforming code segment
LABEL_DESC_VIDEO:       Descriptor      0B8000h,    0FFFFh,     DA_DRW              ;; addr of video memory，文本显示模式下显储地址空间的段值是B800h
;; end GDT

GdtLen      equ     $ - LABEL_GDT   ;; GDT length
GdtPtr      dw      GdtLen - 1      ;; GDT limit
            dd      0               ;; GDT base addr

;; GDT selector，selector是一个索引
SelectorCode32      equ     LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo       equ     LABEL_DESC_VIDEO - LABEL_GDT

;; END of [SECTION .gdt]

[SECTION .s16]
[BITS   16]
LABEL_BEGIN:
        mov     ax,     cs
        mov     ds,     ax
        mov     es,     ax
        mov     ss,     ax
        mov     sp,     0100h

        ;; initialize descriptor of 32bits code segment
        xor     eax,    eax
        mov     ax,     cs
        shl     eax,    4
        add     eax,    LABEL_SEG_CODE32         ;;实模式下求得LABEL_SEG_CODE32的地址值
        mov     word [LABEL_DESC_CODE32 + 2],   ax    ;;将实模式下求得的LABEL_SEG_CODE32地址赋给LABEL_DESC_CODE32描述符
        shr     eax,    16
        mov     byte [LABEL_DESC_CODE32 + 4],   al
        mov     byte [LABEL_DESC_CODE32 + 7],   ah

        ;; prepare for loading gdtr
        xor     eax,    eax
        mov     ax,     ds
        shl     eax,    4
        add     eax,    LABEL_GDT       ;; eax &lt; - gdt base addr
        mov     dword [GdtPtr + 2],     eax     ;; [GdtPtr + 2] &lt;- gdt base addr

        ;; load GDTR
        lgdt    [GdtPtr]

        ;; close interupt
        cli

        ;; open addr bus A20
        in      al,     92h
        or      al,     00000010b
        out     92h,    al 

        ;; prepare for swithing to pmode，cr0的PE位设为1，表示在保护模式下运行
        mov     eax,    cr0
        or      eax,    1
        mov     cr0,    eax

        ;; jmp into pmode，32位的跳转
        jmp     dword SelectorCode32:0
;; END of [SECTION .s16]

[SECTION .s32]
[BITS   32]
LABEL_SEG_CODE32:
        mov     ax,     SelectorVideo
        mov     gs,     ax

        mov     edi,    (80 * 11 + 79) * 2          ;; row 11, col 79
        mov     ah,     0Ch
        mov     al,     'P'
        mov     [gs:edi],   ax

        ;; stop here
        jmp     $

SegCode32Len        equ     $ - LABEL_SEG_CODE32
;; END of [SECTION .s32]</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/03/27/rmode-to-pmode/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>一致代码段与特权级</title>
		<link>http://blog.endlesscode.com/2010/03/20/conforming-code-segment-and-privilege/</link>
		<comments>http://blog.endlesscode.com/2010/03/20/conforming-code-segment-and-privilege/#comments</comments>
		<pubDate>Sun, 21 Mar 2010 06:21:11 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=437</guid>
		<description><![CDATA[一、一致代码段与非一致代码段 “一致”的意思大约是这样，当转移的目标是一个特权级更高的一致代码段时，当前的特权级会被延续下去，而向特权级更高的非一致代码段的转移则会引起常规的保护异常，除非使用调用门或者任务门。一致代码段往往是用在内核共享的段，这些段是允许应用程序去访问的，而不需要内核转移到应用程序中来访问这些共享的资源。对于一致代码段，有以下规则： 特权级高的程序不允许访问特权级低的数据。比如，核心态不允许调用用户态的数据。 特权级低的程序可以访问到特权级高的数据，但是访问程序的特权级不会发生变化。比如，用户态访问内核态共享的资源时，不会变成内核态。 而对于非一致代码段，则有着不同的规则： 只允许同级访问。 绝对禁止不同级程序访问。即内核态不能访问用户态，用户态也不能访问内核态。 之所以这么做是为了系统的安全性考虑，分离内核和用户程序，使内核不能被用户程序干涉，同时避免使用户态程序修改内核态的逻辑，导致在内核态下执行用户程序的代码。上面所说的是代码段，而数据段则全都是非一致的，这意味着不可能被低特权级的代码访问到。然而，与代码段不同的是，数据段可以被更高特权级的代码访问到，而不需要使用特定的门。规则总如如下： 特权级 低-&#62;高 特权级 高-&#62;低 特权级 同级之间 适用于何种代码 一致代码段 Yes No Yes 不访问受保护的资源和某些类型的异常处理的系统代码 非一致代码段 No No Yes 避免低特权级的程序访问而被保护起来的系统代码 数据段 （总是非一致） No Yes Yes 二、特权级 1.CPL、RPL和DPL CPL是当前进程的权限级别(Current Privilege Level)，是当前正在执行的代码所在的段的特权级，存在于cs寄存器的低两位。 RPL说明的是进程对段访问的请求权限(Request Privilege Level)， 是对于段选择子而言的，每个段选择子有自己的RPL，它说明的是进程对段访问的请求权限，有点像函数参数。而且RPL对每个段来说不是固定的，两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用，例如当前CPL=0的进程要访问一个数据段，它把段选择符中的RPL设为3，这样它对该段仍然只有特权为3的访问权限。处理器通过检查RPL和CPL来确认一下请求是否合法。即便提出访问请求的段有足够的特权级，如果RPL不够也是不行的。 操作系统过程往往用RPL来避免低特权级应用程序访问高特权级段内的数据。当操作系统过程（被调用过程）从一个应用程序（调用过程）接收到一个选择子时，将会把选择子的RPL设成调用者的特权级。于是，当操作系统用这个选择子去访问相应的段时，处理器将会调用过程的特权级（已经被存到RPL中），而不是更高的操作过程的特权级（CPL）进行特权检验。这样，RPL就保证了操作系统不会越俎代疱地代表一个应用程序去访问一个段，除非这个程序本身是有权限的。（可以认为是以CPL来访问段DPL所出示的“证件(RPL)”，如出示的“证件”权级范围在CPL之内且满足DPL的特权检查规则：DPL &#8230; <a href="http://blog.endlesscode.com/2010/03/20/conforming-code-segment-and-privilege/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<h2>一、一致代码段与非一致代码段</h2>
<p>“一致”的意思大约是这样，当转移的目标是一个特权级更高的一致代码段时，当前的特权级会被延续下去，而向特权级更高的非一致代码段的转移则会引起常规的保护异常，除非使用调用门或者任务门。一致代码段往往是用在内核共享的段，这些段是允许应用程序去访问的，而不需要内核转移到应用程序中来访问这些共享的资源。对于一致代码段，有以下规则：</p>
<ul>
<li>特权级高的程序不允许访问特权级低的数据。比如，核心态不允许调用用户态的数据。</li>
<li>特权级低的程序可以访问到特权级高的数据，但是访问程序的特权级不会发生变化。比如，用户态访问内核态共享的资源时，不会变成内核态。</li>
</ul>
<p>而对于非一致代码段，则有着不同的规则：</p>
<ul>
<li>只允许同级访问。</li>
<li>绝对禁止不同级程序访问。即内核态不能访问用户态，用户态也不能访问内核态。</li>
</ul>
<p>之所以这么做是为了系统的安全性考虑，分离内核和用户程序，使内核不能被用户程序干涉，同时避免使用户态程序修改内核态的逻辑，导致在内核态下执行用户程序的代码。上面所说的是代码段，而数据段则全都是非一致的，这意味着不可能被低特权级的代码访问到。然而，与代码段不同的是，数据段可以被更高特权级的代码访问到，而不需要使用特定的门。规则总如如下：</p>
<table style="margin: 2px auto; height: 154px; width: 90%;" border="1" cellspacing="0" bordercolor="#000000">
<thead>
<tr>
<td></td>
<td><strong>特权级<br />
低-&gt;高</strong></td>
<td><strong>特权级<br />
高-&gt;低</strong></td>
<td><strong>特权级<br />
同级之间</strong></td>
<td><strong>适用于何种代码</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>一致代码段</td>
<td>Yes</td>
<td>No</td>
<td>Yes</td>
<td>不访问受保护的资源和某些类型的异常处理的系统代码</td>
</tr>
<tr>
<td>非一致代码段</td>
<td>No</td>
<td>No</td>
<td>Yes</td>
<td>避免低特权级的程序访问而被保护起来的系统代码</td>
</tr>
<tr>
<td>数据段<br />
（总是非一致）</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
<td></td>
</tr>
</tbody>
</table>
<p><span id="more-437"></span></p>
<h2>二、特权级</h2>
<h3>1.CPL、RPL和DPL</h3>
<p><strong>CPL</strong>是当前进程的权限级别(<strong>Current Privilege Level</strong>)，是当前正在执行的代码所在的段的特权级，存在于cs寄存器的低两位。</p>
<p align="left"><strong>RPL</strong>说明的是进程对段访问的请求权限(<strong>Request Privilege Level</strong>)， 是对于段选择子而言的，每个段选择子有自己的RPL，它说明的是进程对段访问的请求权限，有点像函数参数。而且RPL对每个段来说不是固定的，两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用，例如当前CPL=0的进程要访问一个数据段，它把段选择符中的RPL设为3，这样它对该段仍然只有特权为3的访问权限。处理器通过检查RPL和CPL来确认一下请求是否合法。即便提出访问请求的段有足够的特权级，如果RPL不够也是不行的。</p>
<p align="left">操作系统过程往往用RPL来避免低特权级应用程序访问高特权级段内的数据。当操作系统过程（被调用过程）从一个应用程序（调用过程）接收到一个选择子时，将会把选择子的RPL设成调用者的特权级。于是，当操作系统用这个选择子去访问相应的段时，处理器将会调用过程的特权级（已经被存到RPL中），而不是更高的操作过程的特权级（CPL）进行特权检验。这样，RPL就保证了操作系统不会越俎代疱地代表一个应用程序去访问一个段，除非这个程序本身是有权限的。（可以认为是以CPL来访问段DPL所出示的“证件(RPL)”，如出示的“证件”权级范围在CPL之内且满足DPL的特权检查规则：DPL &gt;= max{CPL,RPL}，就能正常通过DPL；反之则不会通过还会发生错误）</p>
<p align="left"><strong>DPL</strong>存储在段描述符中，规定访问该段的权限级别(<strong>Descriptor Privilege Level</strong>)，每个段的DPL固定。当进程访问一个段时，需要进程特权级检查，一般要求<strong>DPL &gt;= max {CPL, RPL}</strong>。当当前代码段试图访问一个段或者门时，DPL将会和CPL以及段或门选择子的RPL相比较，根据段或者门类型的不同，DPL将会被区别对待，下面介绍一下各种类型的段或者门的情况：</p>
<ul>
<li>数据段：DPL规定了可以访问此段的<strong>最低特权级</strong>。比如，一个数据段的DPL是1，那么只有运行在CPL为0或者1的程序才有权访问它。</li>
<li>非一致代码段（不使用调用门的情况下）：DPL规定访问此段的特权级。比如，一个非一致代码的特权级为0，那么只有CPL为0的程序才可以访问它。</li>
<li>调用门：DPL规定了当前执行的程序或任务可以访问此调用门的<strong>最低特权级</strong>（这与数据段的规定是一致的）。</li>
<li>一致代码段：DPL规定了访问此段的<strong>最高特权级</strong>。比如，一个一致代码段的DPL是2，那么CPL为0和1的程序将无法访问此段。</li>
<li>TSS：DPL规定了可以访问此TSS的<strong>最低特权级</strong>（这与数据段的规定是一致的）。</li>
</ul>
<h3>2.代码间跳转（段间）</h3>
<p align="left"><strong>普通转跳（没有使用调用门）：</strong>即JMP或CALL后跟着48位全指针（16位段选择子+32位地址偏移），且其中的段选择子指向代码段描述符，这样的跳转称为直接（普通）跳转。<strong>普通跳转不能使特权级发生跃迁，即不会引起CPL的变化</strong>，看下面的详细描述：</p>
<table style="margin: 2px auto; width: 90%;" border="0">
<thead>
<tr>
<td style="width:15%;">代码段</td>
<td>要求</td>
<td style="width:20%;">特权变化</td>
</tr>
</thead>
<tbody>
<tr>
<td>一致代码段</td>
<td>CPL &gt;= DPL ，RPL不检查，也说是一致代码段描述符中的DPL规定可以转移到一致的代码段的最内层特权级（3级可以转移到0级，而0级只能转移到0级）。一致代码段描述符内DPL的这种解释，正好与正常的DPL的解释相反。这是为了提供对应用程序的共享支持，而不要求改变特权级。</td>
<td>转跳后程序的CPL = 转跳前程序的CPL</td>
</tr>
<tr>
<td>非一致代码段</td>
<td>CPL = DPL &amp; RPL&lt;= DPL</td>
<td>转跳后程序的CPL = 转跳前程序的CPL</td>
</tr>
</tbody>
</table>
<p align="left">
<p align="left"><strong>通过调用门的跳转：</strong>当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时，该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移，也不会被cpu使用，因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤，即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限，<strong>要求指示调用门的选择子的 RPL≤门描述符DPL，同时当前代码段CPL≤门描述符DPL</strong>，就如同访问数据段一样，要求访问数据段的程序的CPL≤待访问的数据段的DPL，同时选择子的RPL≤待访问的数据段或堆栈段的DPL。只有满足了以上条件，CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移，进行下一步的操作。</p>
<p align="left"><strong> </strong>从调用门中读取到目标代码的段选择子和地址偏移后，我们当前掌握的信息又回到了先前，和普通跳转站在了同一条起跑线上（普通跳转一开始就得到了目标代码的段选择子和地址偏移），有所不同的是，此时，<strong>CPU会将读到的目标代码段选择子中的RPL清0，即忽略了调用门中代码段选择子的RPL的作用</strong>。完成这一步后，CPU开始对当前程序的CPL，目标代码段选择子的RPL（事实上它被清0后总能满足要求）以及由<strong>目标代码选择子指示的目标代码段描述符中的DPL</strong>进行特权级检查，并根据情况进行跳转，具体情况如下：</p>
<table style="margin: 2px auto; width: 90%;" border="0">
<thead>
<tr>
<td style="width:15%;">代码段</td>
<td>要求</td>
<td>特权变化</td>
</tr>
</thead>
<tbody>
<tr>
<td>一致代码段</td>
<td>CPL &gt;= DPL ，RPL不检查</td>
<td>因为RPL被清0，所以事实上永远满足RPL &lt;= DPL，这一点与普通跳转一致，适用于JMP和CALL。<br />
转跳后程序的CPL = 转跳前程序的CPL，因此特权级没有发生跃迁。</td>
</tr>
<tr>
<td>非一致代码段(JMP)</td>
<td>CPL = DPL　（RPL被清0，不检查）<br />
,若不满足要求则程序引起异常。</td>
<td>转跳后程序的CPL = DPL,<br />
因为前提是CPL=DPL，所以转跳后程序的CPL = DPL不会改变CPL的值，特权级也没有发生变化。如果访问时不满足前提CPL=DPL，则引发异常。</td>
</tr>
<tr>
<td>非一致代码段(CALL)</td>
<td>CPL &gt;= DPL（RPL被清0，不检查），若不满足要求则程序引起异常。</td>
<td>转跳后程序的CPL = DPL,<br />
当条件CPL=DPL时，程序跳转后CPL=DPL,特权级不发生跃迁；当CPL＞DPL时，程序跳转后CPL=DPL,特权级发生跃迁，这是我们当目前位置唯一见到的使程序当前执行优先级(CPL)发生变化的跳转方法，即用CALL指令+调用门方式跳转，且目标代码段是非一致代码段。</td>
</tr>
</tbody>
</table>
<p>参考：</p>
<ul>
<li><a href="http://hi.baidu.com/wangpeng1314/blog/item/153f2b643a0520f9f6365442.html" target="_blank">一致代码段，非一致代码段，特权级，代码间跳转[总结]</a></li>
<li><a href="http://hi.baidu.com/walkingman520/blog/item/4463e21133a42513b9127b91.html" target="_blank">一致代码段和非一致代码段</a></li>
<li>《Orange&#8217;S 一个操作系统的实现》</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/03/20/conforming-code-segment-and-privilege/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>保护模式</title>
		<link>http://blog.endlesscode.com/2010/03/18/protection-mode/</link>
		<comments>http://blog.endlesscode.com/2010/03/18/protection-mode/#comments</comments>
		<pubDate>Thu, 18 Mar 2010 16:52:27 +0000</pubDate>
		<dc:creator>Stephen</dc:creator>
				<category><![CDATA[Operating System]]></category>

		<guid isPermaLink="false">http://blog.endlesscode.com/?p=391</guid>
		<description><![CDATA[一、保护模式简述 在保护模式下，全部32条地址线有效，可寻址高达4G字节的物理地址空间；扩充的存储器分段管理机制和可选的存储器分页管理机制，不仅为存储器共享和保护提供了硬件支持，而且为实现虚拟存储器提供了硬件支持；支持多任务，能够快速地进行任务切换和保护任务环境；4个特权级和完善的特权检查机制，既能实现资源共享以能保证代码及数据的安全和保密、及任务的隔离。 80386采用称为描述符（descriptor）的数据来描述段的位置、大小和使用情况。虚拟存储器的地址（逻辑地址）由指示描述符的选择子（selector）和段内偏移两部分构成，这样的地址集合称为虚拟地址空间，但要注意的是，这与实模式下的段值+偏移量来获取物理地址是不同的。在保护模式下，由选择子和段内偏移计算出来的只是虚拟地址，需要将虚拟地址转换成实际的物理地址。 地址映射转换示意 80386分两步实现虚拟地址空间到物理地址空间的映射，其中第二步是可选的。分页管理机制把线性地址空间和物理地址空间分别划分为大小相同的块，这样的块称之为页。通过在线性空间的页与物理地址空间的页之间建立的映射表，分页管理机制实现线性地址到物理地址空间的映射，实现线性地址到物理地址的转换。分页管理机制是可选的，在不采用分页管理机制时，线性地址空间就直接等同于物理地址空间，线性地址就直接等于物理地址。 在保护模式中，在一个任务之内，定义有4种执行特权级别，用于限制对任务中的段进行访问。特权级别分别用0～3表示，数字0表示最高特权级别，而数字3表示最低特权级别。在任何时候，一个任务总是在4个特权级别之一下运行 ，任务在特定时刻的特权级称为当前特权级（Current Priviledge Level），标记为CPL。而每个存储器段也都与一个特权级别相联系。每当一个程序试图访问一个段时，就把 CPL与要访问的段的特权级进行比较，以决定是否允许这一次访问。 4层特权级别 特权级的典型用法是，把操作系统的核心放在内层的0级，操作系统的其余部分放在1级，而应用程序放在3级，留下2级供中间软件使用。 二、分段管理机制 1.段定义和地址转换 上面的图描述了分段和分页机制下逻辑地址到物理地址的详细转换过程。简单地说，就是段选择子确定了段描述符，由段描述符和偏移确定了线性地址，线性地址再由分页机制转换成相应的物理地址。 段是实现虚拟地址空间到线性地址转换机制的基础。在保护方式下，每个段由如下三个参数进行定义：段基地址（Base Address）、段界限（Limit）和段属性（Attributes）。 段基地址规定线性地址空间中段的开始地址。在80386保护方式下，段基地址长32位。因为基地址长度与寻址长度相同，所以任何一个段都可以从32位线性地址空间中的任何一个字节开始，而不像实方式下规定段的边界必须被16整除。 段界限规定了段的大小。在80386保护模式下，段界限用20位表示，而且段界限可以是以字节为单位或以4K字节为单位。段属性中有一位对此进行定义，该位称为粒度位，以符号G标记。G=0表示段界限以字节为单位，此时20位的段界限可表示1字节~1M字节，增量为1字节；G=1表示段界限以4K字节为单位，此时20位的段界限可表示的范围是4K字节到4G字节，增量为4K字节。段属性中还有一个标记ED表示段的扩展方向位，用以区别数据段和堆栈段的扩展方向位。 2.存储段描述符 用于表示上述段的三个参数的数据称为描述符。每个描述符长8个字节。在保护模式下，每一个段都有一个相应的描述符来描述。按描述符所描述的对象来划分，描述符可分为如下三类：存储段描述符、系统段描述符、门描述符（控制描述符）。 存储段属性描述如下： P：说明所描述的段是否存在，P=1表示描述符所描述的段存在于内存中，P=0表示描述符所描述的段不在内存中。 DPL：为描述符所描述段的特权级，只有有效特权级EPL大于等于DPL时，才能对段进行访问。 DT：存储段描述为1，以区别于系统段描述符和门描述符（DT=0）。 TYPE：说明存储段描述符所描述存储段的具体属性 位0表示被描述的段是否被访问过，该位为0表示未被访问过，为1则表示该段先前已经被访问过。 位1的定义在于描述符所描述段的类型。当所描述的是代码段时，该位指示所描述的代码段是否可读，为1则可读，为0则不可读；当所描述的是数据段时，该位指示所描述的数据段是否可写，为1则可写，为0则不可写。 位2的定义也在于描述符所描述段的类型。当所描述的是代码段时，该位指示所描述的代码段是否是一致代码段，为1表示该代码段是一致代码段，为0表示该代码段不是一致代码段，即是普通的代码段。当所描述的是数据段时，该位指示该段的扩展方向，为1时表示该段向低地址扩展，为0时表示该段向高地址扩展。 位3指示所描述的段类型。为1表示所描述的段是代码段，是可以被执行的；为0表示所描述的段是数据段，是不能被执行的。前面已经说了，在保护模式下应该把堆栈段理解为特殊的数据段，为0时也包括堆栈段。 G：段界限粒度位，表示段界限的计数单位，该位为0时表示段界限以字节为单位，为1时表示以4K为单位。这样计算下来，20位的段界限就可以描述大小为64K或4G的段了。 D/B：说明描述符所描述的段是32位环境还是16位的环境。 在可执行代码段描述符中，这一位叫做D位。D＝1时，在默认情况下指令使用32位地址及32位或8位操作数；D＝0时，在默认情况下，使用16位地址及16位或8位操作数。 在向下扩展数据段的描述符中，这一位叫做B位。B＝1时，段的上部界限为4GB；B＝0时，段的上部界限为64KB。 在描述堆栈段（由ss寄存器指向的段）的描述符中，这一位叫做B位。B＝1时，隐式的堆栈访问指令（如push、pop和call）使用32位堆栈指针寄存器esp；B＝0时，隐式的堆栈访问指令（如push、pop和call）使用16位堆栈指针寄存器sp。 AVL：是软件可用位。80386对该位的使用未做规定，Intel公司也保证今后开发生产的处理只要与80386兼容，就不会对该位的使用做任何定义或规定。 3.全局和局部描述符表 描述符表是由描述符组成的线性表。在80386中有三种类型的描述符表：全局描述符表GDT、局部描述符表LDT和中断描述符表IDT。在整个系统中，全局描述符表GDT和中断描述符表IDT只有一张。局部描述符表可以有若干张，每个任务可以有一张。每个描述符表本身形成一个特殊的数据段。这样的特殊数据段最多可以含有8K个描述符（因为段选择子用于表示索引的只有13位）。 每个任务的局部描述符表LDT含有该任务自己的代码段、数据段和堆栈段的描述符，也包含该任务所使用的一些门描述符。随着任务的切换，系统当前的局部描述符表LDT也随之切换。全局描述符表GDT含有每一个任务都可能或可以访问的段描述符，通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符，也包含多种特殊数据段的描述符。在任务切换时，并不切换GDT。 4.段选择子 &#8230; <a href="http://blog.endlesscode.com/2010/03/18/protection-mode/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<h2>一、保护模式简述</h2>
<p>在保护模式下，全部32条地址线有效，可寻址高达4G字节的物理地址空间；扩充的存储器分段管理机制和可选的存储器分页管理机制，不仅为存储器共享和保护提供了硬件支持，而且为实现虚拟存储器提供了硬件支持；支持多任务，能够快速地进行任务切换和保护任务环境；4个特权级和完善的特权检查机制，既能实现资源共享以能保证代码及数据的安全和保密、及任务的隔离。</p>
<p>80386采用称为<strong>描述符</strong>（descriptor）的数据来描述段的位置、大小和使用情况。虚拟存储器的地址（<strong>逻辑地址</strong>）由指示描述符的<strong>选择子（selector）</strong>和<strong>段内偏移</strong>两部分构成，这样的地址集合称为虚拟地址空间，但要注意的是，这与实模式下的段值+偏移量来获取物理地址是不同的。在保护模式下，由选择子和段内偏移计算出来的只是虚拟地址，需要将虚拟地址转换成实际的物理地址。</p>
<p style="text-align: center;"><img class="aligncenter size-full wp-image-396" title="transformaddr" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/transformaddr1.jpg" alt="transformaddr" width="522" height="161" />地址映射转换示意<span id="more-391"></span></p>
<p style="text-align: left;">80386分两步实现虚拟地址空间到物理地址空间的映射，其中第二步是可选的。<strong>分页管理机制</strong>把线性地址空间和物理地址空间分别划分为大小相同的块，这样的块称之为页。通过在线性空间的页与物理地址空间的页之间建立的映射表，分页管理机制实现线性地址到物理地址空间的映射，实现线性地址到物理地址的转换。分页管理机制是可选的，在不采用分页管理机制时，线性地址空间就直接等同于物理地址空间，线性地址就直接等于物理地址。</p>
<p>在保护模式中，在一个任务之内，定义有4种执行特权级别，用于限制对任务中的段进行访问。特权级别分别用0～3表示，数字0表示最高特权级别，而数字3表示最低特权级别。在任何时候，一个任务总是在4个特权级别之一下运行 ，任务在特定时刻的特权级称为当前特权级（Current Priviledge Level），标记为CPL。而每个存储器段也都与一个特权级别相联系。每当一个程序试图访问一个段时，就把 CPL与要访问的段的特权级进行比较，以决定是否允许这一次访问。</p>
<p><img class="aligncenter size-full wp-image-401" title="priv" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/priv.JPG" alt="priv" width="372" height="344" /></p>
<p style="text-align: center;">4层特权级别</p>
<p>特权级的典型用法是，把操作系统的核心放在内层的0级，操作系统的其余部分放在1级，而应用程序放在3级，留下2级供中间软件使用。</p>
<h2>二、分段管理机制</h2>
<h3>1.段定义和地址转换</h3>
<p style="text-align: center;"><img class="aligncenter" title="segmentationpage" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/segmentationpage.jpg" alt="segmentationpage" width="577" height="487" /></p>
<p>上面的图描述了分段和分页机制下逻辑地址到物理地址的详细转换过程。<strong>简单地说，就是段选择子确定了段描述符，由段描述符和偏移确定了线性地址，线性地址再由分页机制转换成相应的物理地址。</strong></p>
<p>段是实现虚拟地址空间到线性地址转换机制的基础。在保护方式下，每个段由如下三个参数进行定义：段基地址（Base Address）、段界限（Limit）和段属性（Attributes）。</p>
<p>段基地址规定线性地址空间中段的开始地址。在80386保护方式下，段基地址长32位。因为基地址长度与寻址长度相同，所以任何一个段都可以从32位线性地址空间中的任何一个字节开始，而不像实方式下规定段的边界必须被16整除。</p>
<p>段界限规定了段的大小。在80386保护模式下，段界限用20位表示，而且段界限可以是以字节为单位或以4K字节为单位。段属性中有一位对此进行定义，该位称为粒度位，以符号G标记。G=0表示段界限以字节为单位，此时20位的段界限可表示1字节~1M字节，增量为1字节；G=1表示段界限以4K字节为单位，此时20位的段界限可表示的范围是4K字节到4G字节，增量为4K字节。段属性中还有一个标记ED表示段的扩展方向位，用以区别数据段和堆栈段的扩展方向位。</p>
<h3>2.存储段描述符</h3>
<p>用于表示上述段的三个参数的数据称为描述符。每个描述符长8个字节。在保护模式下，每一个段都有一个相应的描述符来描述。按描述符所描述的对象来划分，描述符可分为如下三类：存储段描述符、系统段描述符、门描述符（控制描述符）。</p>
<p><img class="aligncenter size-full wp-image-421" title="segment" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/segment.jpg" alt="segment" width="530" height="621" /></p>
<p>存储段属性描述如下：</p>
<ul>
<li>P：说明所描述的段是否存在，P=1表示描述符所描述的段存在于内存中，P=0表示描述符所描述的段不在内存中。</li>
<li>DPL：为描述符所描述段的特权级，只有有效特权级EPL大于等于DPL时，才能对段进行访问。</li>
<li>DT：存储段描述为1，以区别于系统段描述符和门描述符（DT=0）。</li>
<li>TYPE：说明存储段描述符所描述存储段的具体属性
<ul>
<li>位0表示被描述的段是否被访问过，该位为0表示未被访问过，为1则表示该段先前已经被访问过。</li>
<li>位1的定义在于描述符所描述段的类型。当所描述的是代码段时，该位指示所描述的代码段是否可读，为1则可读，为0则不可读；当所描述的是数据段时，该位指示所描述的数据段是否可写，为1则可写，为0则不可写。</li>
<li>位2的定义也在于描述符所描述段的类型。当所描述的是代码段时，该位指示所描述的代码段是否是一致代码段，为1表示该代码段是一致代码段，为0表示该代码段不是一致代码段，即是普通的代码段。当所描述的是数据段时，该位指示该段的扩展方向，为1时表示该段向低地址扩展，为0时表示该段向高地址扩展。</li>
<li>位3指示所描述的段类型。为1表示所描述的段是代码段，是可以被执行的；为0表示所描述的段是数据段，是不能被执行的。前面已经说了，在保护模式下应该把堆栈段理解为特殊的数据段，为0时也包括堆栈段。</li>
</ul>
</li>
<li>G：段界限粒度位，表示段界限的计数单位，该位为0时表示段界限以字节为单位，为1时表示以4K为单位。这样计算下来，20位的段界限就可以描述大小为64K或4G的段了。</li>
<li>D/B：说明描述符所描述的段是32位环境还是16位的环境。
<ul>
<li>在可执行代码段描述符中，这一位叫做D位。D＝1时，在默认情况下指令使用32位地址及32位或8位操作数；D＝0时，在默认情况下，使用16位地址及16位或8位操作数。</li>
<li>在向下扩展数据段的描述符中，这一位叫做B位。B＝1时，段的上部界限为4GB；B＝0时，段的上部界限为64KB。</li>
<li>在描述堆栈段（由ss寄存器指向的段）的描述符中，这一位叫做B位。B＝1时，隐式的堆栈访问指令（如push、pop和call）使用32位堆栈指针寄存器esp；B＝0时，隐式的堆栈访问指令（如push、pop和call）使用16位堆栈指针寄存器sp。</li>
</ul>
</li>
<li>AVL：是软件可用位。80386对该位的使用未做规定，Intel公司也保证今后开发生产的处理只要与80386兼容，就不会对该位的使用做任何定义或规定。</li>
</ul>
<h3>3.全局和局部描述符表</h3>
<p>描述符表是由描述符组成的线性表。在80386中有三种类型的描述符表：全局描述符表GDT、局部描述符表LDT和中断描述符表IDT。在整个系统中，<strong>全局描述符表GDT和中断描述符表IDT只有一张</strong>。局部描述符表可以有若干张，每个任务可以有一张。每个描述符表本身形成一个特殊的数据段。这样的特殊数据段最多可以含有8K个描述符（因为段选择子用于表示索引的只有13位）。</p>
<p>每个任务的局部描述符表LDT含有该任务自己的代码段、数据段和堆栈段的描述符，也包含该任务所使用的一些门描述符。随着任务的切换，系统当前的局部描述符表LDT也随之切换。全局描述符表GDT含有每一个任务都可能或可以访问的段描述符，通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符，也包含多种特殊数据段的描述符。在任务切换时，并不切换GDT。</p>
<h3>4.段选择子</h3>
<p>在实方式下，逻辑地址空间中存储单元的地址有段值和段内偏移两部分组成。在保护方式下，虚拟地址空间（逻辑地址）中存储单元的地址有段选择子和段内偏移两部分组成。与实方式相比，段选择子替代了段值。</p>
<p style="text-align: center;"><img class="aligncenter size-full wp-image-423" title="selector" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/selector.jpg" alt="selector" width="354" height="171" />段选择子</p>
<p>段选择子长16位，具体的属性如下：</p>
<ul>
<li>高13位是描述符索引。所谓描述符索引是指描述符在描述符表中的序号。</li>
<li>T1位是引用描述符表指示位。T1＝0指示从全局描述符表GDT中读取描述符；T1＝1指示从局部描述符表LDT中读取描述符。</li>
<li>RPL是请求特权级，用于特权检查。</li>
</ul>
<p>段选择子确定描述符，描述符确定段基地址，由段基地址和偏移之和就是线性地址。</p>
<h2>三、控制寄存器和系统地址寄存器</h2>
<h3>1.控制寄存器</h3>
<p>80386有4个32位控制寄存器，分别命名为CR0、CR1、CR2、CR3。但CR1被保留，供今后开发的处理器使用，在80386中不能使用CR1，否则将引起无效指令操作异常。CR0包含指示处理器工作方式的控制位，包含启用和禁用分页管理机制的控制位，包含控制浮点协处理器操作的控制位。CR2及CR3由分页管理机制使用。CR0中的位5~位30及CR3中的位0~位11是保留位，这些位不能随意值，必须为0。</p>
<p><img class="aligncenter size-full wp-image-425" title="cr" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/cr.jpg" alt="cr" width="527" height="240" /></p>
<p>控制寄存器CR0中的位0用PE标记，位31用PG标记，这两个位控制分段和分页管理机制的操作。把它们称保护控制位。PE控制分段管理机制，PE=0处理器运行于实方式，PE=1处理器运行于保护模式。PG控制分页管理机制，PG=0禁用分页管理机制，PG=1启用分页管理机制。</p>
<h3>2.系统地址寄存器</h3>
<p>全局描述符表GDT、局部描述符表LDT和中断描述符表IDT等是保护模式下非常重要的特殊段，它们包含有对段机制所用的重要表格。为了方便快捷地定位这些段，处理器采用一些特殊的寄存器保存这些段的基地址和界限。这些特殊的寄存器就称为系统地址寄存器。</p>
<p><img class="aligncenter size-full wp-image-426" title="gdtr" src="http://blog.endlesscode.com/wp-content/uploads/2010/03/gdtr.jpg" alt="gdtr" width="465" height="143" /></p>
<p>全局描述符表寄存器GDTR，长48位，其中高32位含基地址，低16位含界限。GDTR是用于保存的首个字节所在的线性内存地址。</p>
<p>局部描述符表寄存LDTR，由程序员可见的一个16位寄存器和程序员不可见的高速缓冲寄存器组成。实际上，每个任务的局部描述符表LDT作为系统的一个特殊段，由一个描述符描述，而用于描述LDT的描述符存入在GDT中。</p>
<p><span style="font-family: 宋体;">中断描述符表寄存器IDTR，</span>与GDTR<span style="font-family: 宋体;">的作用类似，IDTR<span style="font-family: 宋体;">寄存器用于存放中断描述符表IDT<span style="font-family: 宋体;">的32<span style="font-family: 宋体;">位线性基地址和16<span style="font-family: 宋体;">位表长度值。指令LIDT<span style="font-family: 宋体;">和SIDT<span style="font-family: 宋体;">分别用于加载和保存IDTR<span style="font-family: 宋体;">寄存器的内容。在机器刚加电或处理器复位后，基地址被默认地设置为0<span style="font-family: 宋体;">，而长度值被设置成0xFFFF。</span></span></span></span></span></span></span></span></span></p>
<p><span style="font-family: 宋体;">任务寄存器TR，</span>TR寄存器用于存放当前任务<span lang="EN-US">TSS段的16<span style="font-family: 宋体;">位段选择符、32<span style="font-family: 宋体;">位基地址、16<span style="font-family: 宋体;">位段长度和描述符属性值。它引用GDT<span style="font-family: 宋体;">表中的一个TSS<span style="font-family: 宋体;">类型的描述符。指令LTR<span style="font-family: 宋体;">和STR<span style="font-family: 宋体;">分别用于加载和保存TR<span style="font-family: 宋体;">寄存器的段选择符部分。当使用LTR<span style="font-family: 宋体;">指令把选择符加载进任务寄存器时，TSS<span style="font-family: 宋体;">描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时，处理器会把新任务的<span lang="EN-US">TSS的段选择符和段描述符自动加载进任务寄存器<span lang="EN-US">TR中。</span></span></span></span></span></span></span></span></span></span></span></span></span></p>
<p><span lang="EN-US"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span lang="EN-US"><span lang="EN-US">引用：</span></span></span></span></span></span></span></span></span></span></span></span></span>《<span lang="EN-US"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span style="font-family: 宋体;"><span lang="EN-US"><span lang="EN-US">80X86汇编语言程序设计教程</span></span></span></span></span></span></span></span></span></span></span></span></span>》</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.endlesscode.com/2010/03/18/protection-mode/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

