<?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>阿当正传 &#187; Linux kernel</title>
	<atom:link href="http://www.adamjiang.com/archives/category/linux-kernel/feed" rel="self" type="application/rss+xml" />
	<link>http://www.adamjiang.com</link>
	<description>长脑袋的个人博客</description>
	<lastBuildDate>Tue, 15 May 2012 16:53:14 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.2</generator>
		<item>
		<title>Linux内核代码阅读序列(10)-内存模型 之二</title>
		<link>http://www.adamjiang.com/archives/818</link>
		<comments>http://www.adamjiang.com/archives/818#comments</comments>
		<pubDate>Fri, 08 Jan 2010 09:19:10 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=818</guid>
		<description><![CDATA[内存区段 x86结构CPU上采用了一种叫内存区段的方式来寻址，这样做最大的好处就是可以保护内存中的内容不被非法访问，在这种内存模型中某个地址可以被表示成两部分，线性地址由这两部分计算得出。 LinearAddress = BaseAddress of Segmentation + Offset 其中，段基值存储在段基值寄存器当中，其值总是指向当前正在使用的内存区域的基地址；在x86处理器的32位保护模式中，一个区段的大小时4GB；这是因为32位CPU上，段基值寄存器的位宽是32位(2^32=4G)。x86的原始设计包含三个段基址寄存器， CS Register:CodeSeg 指令段基址寄存器 DS Register:DataSeg 数据段基址寄存器 SS Register:StackSeg 栈段基址寄存器 同时，x86结构上将一个被叫做段描述符(Segmentation Descriptor)的东西存储在两个表(GDT和LDT)之中，需要使用时从表中取出段描述符来找到线性地址；因为Linux操作系统最初的设计目标就是支持x86结构的计算机，所以，Linux内核在构建内存模型的时候也采取了内存区段的方式，但是，为了简化设计和提高可以执行，内核的做法是只保留一个存储段描述符的表(GDT)和一个段基址寄存器，从而让所有的段都开始于同一个位置，而段的大小也正好成为4GB，也就是一般情况下的整个线性地址空间的大小。正因为如此，在大多数情况下，逻辑地址就可以直接被看作线性地址来使用，而逻辑地址到线性地址的转换事实上时内核或者CPU在对用户透明的情况下完成的，这个转换过程不需要特别注意。需要注意的是这里提到的这些名词，以及那个小学算数一般的地址计算方法。此外，Linux内核代码使用的内存区段和若干个宏也最好记住，因为你知道，好记性对一个程序员来说真tmd重要。 Name Macro Base Limit G S Type DPL KernelCodeSeg __KERNEL_CS 0&#215;00000000 0xffffffff 1 1 0xa 0 KernelDataSeg __KERNEL_DS 0&#215;00000000 0xffffffff 1 1 0&#215;10 0 UserCodeSeg __USER_DS 0&#215;00000000 0xffffffff 1 1 0xa 3 UserDataSeg __USER_DS [...]]]></description>
			<content:encoded><![CDATA[<h4>内存区段</h4>
<p>x86结构CPU上采用了一种叫内存区段的方式来寻址，这样做最大的好处就是可以保护内存中的内容不被非法访问，在这种内存模型中某个地址可以被表示成两部分，线性地址由这两部分计算得出。</p>
<pre>LinearAddress = BaseAddress of Segmentation + Offset</pre>
<p>其中，段基值存储在段基值寄存器当中，其值总是指向当前正在使用的内存区域的基地址；在x86处理器的32位保护模式中，一个区段的大小时4GB；这是因为32位CPU上，段基值寄存器的位宽是32位(2^32=4G)。x86的原始设计包含三个段基址寄存器，</p>
<ul>
<li>CS Register:CodeSeg 指令段基址寄存器</li>
<li>DS Register:DataSeg 数据段基址寄存器</li>
<li>SS Register:StackSeg 栈段基址寄存器</li>
</ul>
<p>同时，x86结构上将一个被叫做段描述符(Segmentation Descriptor)的东西存储在两个表(GDT和LDT)之中，需要使用时从表中取出段描述符来找到线性地址；因为Linux操作系统最初的设计目标就是支持x86结构的计算机，所以，Linux内核在构建内存模型的时候也采取了内存区段的方式，但是，为了简化设计和提高可以执行，内核的做法是只保留一个存储段描述符的表(GDT)和一个段基址寄存器，从而让所有的段都开始于同一个位置，而段的大小也正好成为4GB，也就是一般情况下的整个线性地址空间的大小。正因为如此，在大多数情况下，逻辑地址就可以直接被看作线性地址来使用，而逻辑地址到线性地址的转换事实上时内核或者CPU在对用户透明的情况下完成的，这个转换过程不需要特别注意。需要注意的是这里提到的这些名词，以及那个小学算数一般的地址计算方法。此外，Linux内核代码使用的内存区段和若干个宏也最好记住，因为你知道，好记性对一个程序员来说真tmd重要。</p>
<table border="1">
<thead>
<tr>
<td>Name</td>
<td>Macro</td>
<td>Base</td>
<td>Limit</td>
<td>G</td>
<td>S</td>
<td>Type</td>
<td>DPL</td>
</tr>
</thead>
<tbody>
<tr>
<td>KernelCodeSeg</td>
<td>__KERNEL_CS</td>
<td>0&#215;00000000</td>
<td>0xffffffff</td>
<td>1</td>
<td>1</td>
<td>0xa</td>
<td>0</td>
</tr>
<tr>
<td>KernelDataSeg</td>
<td>__KERNEL_DS</td>
<td>0&#215;00000000</td>
<td>0xffffffff</td>
<td>1</td>
<td>1</td>
<td>0&#215;10</td>
<td>0</td>
</tr>
<tr>
<td>UserCodeSeg</td>
<td>__USER_DS</td>
<td>0&#215;00000000</td>
<td>0xffffffff</td>
<td>1</td>
<td>1</td>
<td>0xa</td>
<td>3</td>
</tr>
<tr>
<td>UserDataSeg</td>
<td>__USER_DS</td>
<td>0&#215;00000000</td>
<td>0xffffffff</td>
<td>1</td>
<td>1</td>
<td>0&#215;10</td>
<td>3</td>
</tr>
</tbody>
<tbody></tbody>
</table>
<h4>线性地址在使用上的划分</h4>
<p>普通的x86结构计算机上，从用户的角度看，线性地址是一个连续内存区域。它的最小地址是<code>0x0000 0000</code>，而最大地址是<code>0xffff ffff</code>。这个区域在概念上被分成两个部分，一部分是会根据不同的进程切换而改变的userspace区域，而另一部分，则在系统启动后一直不变的kernel space区域。这个位置的分界点可在内核代码中通过<code>PAGE_OFFSET</code>宏来定义，而<code>PAGE_OFFSET</code>却来自于内核编译的配置文件<code>.config</code>中的<code>CONFIG_PAGE_OFFSET</code>。在x86结构上，它被定义为<code>0xC000 0000</code>；这意味着，用户空间的大小时3GB，而内核仅有1GB可以使用。</p>
<pre> |&lt;---          3GB          ----&gt;|&lt;----  1GB ----&gt;|
 +--------------------------------+----------------+
 |     userspace                  | kernel space   |
 +--------------------------------+----------------+
                            PAGE_OFFSET
                           (0xC000 0000)</pre>
<p>内核在系统启动时被<strong>Bootloader</strong>——x86结构上常用的是GRUB/2——装载到线性地址的<code>0xC000 0000</code>位置以后，而这个线性地址通常被映射到物理地址的<code>0x0010 0000</code>，也就是物理地址最初的1M以后，关于这里点的证明此后详述。在线性地址空间中，紧随内核装载位置之后，Linux会利用一段内存区域来加载物理内存管理需要使用的数据结构，这个区域的大小一般根据系统上可以使用的RAM的大小而改变，而其装载在物理内存中的位置会根据体系结构的不同而改变，但通常会避开前16M，因为最初的16M被用来给ISA设备提供DMA支持，这是物理内存管理中另外一个恼人的地方，且听下下回分解吧。此前提到的这两个区域都时直接的物理内存映射区域，具体的位置用内核宏定义来说的话应该时开始于<code>PAGE_OFFSET</code>，而结束于<code>(VMALLOC_START-VMALLOC_OFFSET)</code>。-_-!!! 抱歉，又说火星语了，但是除此以外还真没有好的描述语言。</p>
<pre>                             VMALLOC_OFFSET(0x0080 0000)
                                  ¥ | ¥
     +-----------+----------------+---+-------------------+
     |KernelImage|PhysicalMMStruct|Gap| vmalloc Add Space |
     +-----------+----------------+---+-------------------+
     ^                                |
     |                           VMALLOC_START
  PAGE_OFFSET
(0xC000 0000)</pre>
<p>显而易见，<code>VMALLOC_START = SizeOf(PhysicalMMStruct) + VMALLOC_OFFSET</code>。后续的线性地址空间被用以支持内核函数<code>vmalloc()</code>，使用这个函数可以申请在线性地址空间连续的却事实上在物理地址上可能不连续的内存区域；如果在支持<code>HIGH_MEM</code>的系统上，在两个PAGE的间隔以后，会有一块区域来支持将高地址转化到低地址的函数<code>kmap()</code>，这个区域的开始位置是<code>PKMAP_BASE</code>。为什么要转换呢？下下下下回分解&#8230;再接下来，还需要划分一片区域来支持编译时就需要定位的内存模块，比如APIC，这段区域采用固定映射(Fixed Mapping)的方式，将线性地址映射到物理地址，所以，利用链接器脚本或者其他方法可以在编译时确认代码被装载的位置；这片内存区域的开始位置是<code>FIXADDR_START</code>，而结束位置时<code>FIXADDR_TOP</code>；<code>FIXADDR_TOP</code>是<code>0xffff f000</code>，而<code>FIXADDR_START</code>会根据宏<code>__FIXADDR_START</code>计算得出。</p>
<pre>     +--------------------+---+--------------+-------------------------+---+
     | vmalloc Add Space  |Gap|kmap Add space|Fixed Virtual Add Mapping|Gap|
     +--------------------+---+--------------+-------------------------+---+
     |           VMALLOC_END  |              |                         |
     |                  PKMAP_BASE           |                  FIXEADDR_TOP
VMALLOC_START                         FIXADDR_START</pre>
<p>正是因为前面提到<code>vmalloc()</code>，<code>kmap()</code>和固定映射地址，内核物理内存区域管理中用到的<code>ZONE_NORMAL</code>的大小才受到限制，而不能占据整个1GB空间。这个限制通过宏定义<code>VMALLOC_RESERVE</code>体现出来，这个值在不同的体系结构上有所不同，但是在x86结构上，它被定义为128MB，理所当然的，ZONE_NORMAL可以使用的线性地址空间大小就变成了<code>1GB - 128MB = 896MB</code>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/818/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核代码阅读序列(9)-内存模型 之一</title>
		<link>http://www.adamjiang.com/archives/805</link>
		<comments>http://www.adamjiang.com/archives/805#comments</comments>
		<pubDate>Thu, 07 Jan 2010 14:54:56 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=805</guid>
		<description><![CDATA[重新开始 2009年的时候信誓旦旦的说要将这个系列坚持写下去，结果未能如愿。抱歉的很。去年前半年奔波劳碌得到处跑，9月以后自己的人生发生了180度的大转弯，自己给自己的shock就已经够大了，更加没有心思写blog。还好还好，上帝给了我一个此前从未有的幸福的开端，也许我可以牵着她的手，安心下来好好补足功课了。愿上帝保佑2010一切都好。愿上帝保佑我能将这个系列按照原来的计划写完。 Linux的内存模型 理解Linux内核所采用的内存模型是弄懂Linux内存管理的第一步。很多介绍Linux源码阅读的书和文章都会建议新手应该从内存管理部分入手。可是，在我看来这并不是一个非常好的建议，因为如果搞不懂Linux内核中内存管理的模型直接阅读代码的话会迷失其中，一个重要原因就是这个部分很多实装方法都牵扯到了不同体系结构的差别。而Linux为了提高可移植性又对某些体系结构上普遍应用的内存模型进行了抽象。在看代码的时候，应该时刻注意区分那些是抽象了的部分，哪些代码是不同体系结构上的实现。 内存管理需要解决的问题 虚拟内存管理(Virtual Memory Management) 虚拟内存是现代操作系统的一个重要特征；采用虚拟内存的方法可以获得很多有点，比如通过这种抽象可以简化应用程序的开发，可以防止内存非法访问提高安全性等等； 物理内存管理(Physical Memory Management) 操作系统的主要功能之一就是进行资源管理，而内存恰恰就是最重要的系统资源； 内核的虚拟内存管理，内核内存分配器(Allocator) 应用程序或者内核内部模块需要内存时需要向内核管理模块申请，分配器帮助这些组件得到自己想要的内存； 虚拟地址空间管理(Virtual Memory Space) 交换(swap)和缓存(cache) 交换，也是计算机系统中利用局部性(locality)特征的一个重要功能，内存管理模块可以通过交换的方式将暂时用不到的内存页“交”给低速的硬盘保存，而需要时又将这些内存内容“换”回到系统主存之中；缓存也是局部性的一个应用，现代的CPU一般都会有高速缓存，高速缓存在CPU和主存之间充当缓冲，为CPU快速得访问数据和指令提供支持。 x86结构上的地址种类 x86结构上，内存地址被分成3类，理论地址，线性地址和物理地址； 逻辑地址，是从正在运行的应用程序的角度来看，某个数据或者指令出现的位置；这个地址有可能直接就是物理地址，也有可能不是；一般来说，各种控制器，比如DMA控制器，PCI控制器对内核提出内存申请的时候所给参数都是逻辑地址； 线性地址，或者被称为平面地址空间(Flat Memory Address)的地址，事实上就是在程序员脑子里的地址，它就是从0开始每个存储单元顺序增加标号的地址。这也是最原始最简单的地址编排方式，除了Intel之外的很多CPU都采用这种地址编排方式；而Intel体系结构的CPU上采用了分段的地址空间，这种方式将线性地址按照64KB(286结构)或者4GB(386以后)为单位进行分段，而且，段地址寄存器中总是存储这当前要使用的那段内存的基址(base address)。虽然这种方式在32位结构上也可以被看作是一整个平面的地址空间，但事实上它却是分段的； 物理地址就是在总线上表示的那个地址。当物理地址和逻辑地址不一致的时候，通过内存管理单元(MMU)，可以将路基地址转换为物理地址。 CPU使用内存区段单元和分页单元可以将逻辑地址转化成为物理地址； Logical Linear Physical Address +--------------+ Address +-------------+ Address --------&#62;&#124;Segmented Unit&#124;--------&#62;&#124; Paging Unit &#124;---------&#62; +--------------+ +-------------+ &#8211; 参考文献 http://www.ibm.com/developerworks/jp/linux/library/l-memmod/index.html]]></description>
			<content:encoded><![CDATA[<h3>重新开始</h3>
<p>2009年的时候信誓旦旦的说要将这个系列坚持写下去，结果未能如愿。抱歉的很。去年前半年奔波劳碌得到处跑，9月以后自己的人生发生了180度的大转弯，自己给自己的shock就已经够大了，更加没有心思写blog。还好还好，上帝给了我一个此前从未有的幸福的开端，也许我可以牵着她的手，安心下来好好补足功课了。愿上帝保佑2010一切都好。愿上帝保佑我能将这个系列按照原来的计划写完。</p>
<h3>Linux的内存模型</h3>
<p>理解Linux内核所采用的内存模型是弄懂Linux内存管理的第一步。很多介绍Linux源码阅读的书和文章都会建议新手应该从内存管理部分入手。可是，在我看来这并不是一个非常好的建议，因为如果搞不懂Linux内核中内存管理的模型直接阅读代码的话会迷失其中，一个重要原因就是这个部分很多实装方法都牵扯到了不同体系结构的差别。而Linux为了提高可移植性又对某些体系结构上普遍应用的内存模型进行了抽象。在看代码的时候，应该时刻注意区分那些是抽象了的部分，哪些代码是不同体系结构上的实现。</p>
<h4>内存管理需要解决的问题</h4>
<ul>
<li>虚拟内存管理(Virtual Memory Management)
<ul>
<li>虚拟内存是现代操作系统的一个重要特征；采用虚拟内存的方法可以获得很多有点，比如通过这种抽象可以简化应用程序的开发，可以防止内存非法访问提高安全性等等；</li>
</ul>
</li>
<li> 物理内存管理(Physical Memory Management)
<ul>
<li> 操作系统的主要功能之一就是进行资源管理，而内存恰恰就是最重要的系统资源；</li>
</ul>
</li>
<li>内核的虚拟内存管理，内核内存分配器(Allocator)
<ul>
<li> 应用程序或者内核内部模块需要内存时需要向内核管理模块申请，分配器帮助这些组件得到自己想要的内存；</li>
</ul>
</li>
<li> 虚拟地址空间管理(Virtual Memory Space)</li>
<li> 交换(swap)和缓存(cache)
<ul>
<li> 交换，也是计算机系统中利用局部性(locality)特征的一个重要功能，内存管理模块可以通过交换的方式将暂时用不到的内存页“交”给低速的硬盘保存，而需要时又将这些内存内容“换”回到系统主存之中；缓存也是局部性的一个应用，现代的CPU一般都会有高速缓存，高速缓存在CPU和主存之间充当缓冲，为CPU快速得访问数据和指令提供支持。</li>
</ul>
</li>
</ul>
<h4>x86结构上的地址种类</h4>
<p>x86结构上，内存地址被分成3类，理论地址，线性地址和物理地址；</p>
<ol>
<li> <strong>逻辑地址</strong>，是从正在运行的应用程序的角度来看，某个数据或者指令出现的位置；这个地址有可能直接就是物理地址，也有可能不是；一般来说，各种控制器，比如DMA控制器，PCI控制器对内核提出内存申请的时候所给参数都是逻辑地址；</li>
<li> <strong>线性地址</strong>，或者被称为平面地址空间(Flat Memory Address)的地址，事实上就是在程序员脑子里的地址，它就是从0开始每个存储单元顺序增加标号的地址。这也是最原始最简单的地址编排方式，除了Intel之外的很多CPU都采用这种地址编排方式；而Intel体系结构的CPU上采用了分段的地址空间，这种方式将线性地址按照64KB(286结构)或者4GB(386以后)为单位进行分段，而且，段地址寄存器中总是存储这当前要使用的那段内存的基址(base address)。虽然这种方式在32位结构上也可以被看作是一整个平面的地址空间，但事实上它却是分段的；</li>
<li> <strong>物理地址</strong>就是在总线上表示的那个地址。当物理地址和逻辑地址不一致的时候，通过内存管理单元(MMU)，可以将路基地址转换为物理地址。</li>
</ol>
<p>CPU使用内存区段单元和分页单元可以将逻辑地址转化成为物理地址；</p>
<pre>Logical                   Linear                  Physical
Address  +--------------+ Address +-------------+ Address
--------&gt;|Segmented Unit|--------&gt;| Paging Unit |---------&gt;
         +--------------+         +-------------+</pre>
<p>&#8211;<br />
参考文献<br />
<a title="Understanding Linux Memory Model" href="http://www.ibm.com/developerworks/jp/linux/library/l-memmod/index.html" target="_blank">http://www.ibm.com/developerworks/jp/linux/library/l-memmod/index.html</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/805/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(8)-内核的构成 之 二</title>
		<link>http://www.adamjiang.com/archives/374</link>
		<comments>http://www.adamjiang.com/archives/374#comments</comments>
		<pubDate>Tue, 27 Jan 2009 08:08:04 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[技术漫谈]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[源代码]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=374</guid>
		<description><![CDATA[进程管理 进程 进程是正在运行的程序的实体。它们是Linux用以完成各种应用程序的核心。”链接“系列文章中说明了一个应用程序如何从源代码变成可执行文件，以及如何将这个可执行文件加载进入内存从而使程序运行的过程。可执行的目标文件被加载后就行成了进程的基本组成部分。操作系统本身还会为每一个进程添加一些附加的信息用以对进程进行调度管理和创建/消灭等过程。另一方面，为了方便用户的使用，Linux向用户应用程序提供了一些系统调用帮助用户应用程序进行进程的管理。进程有自己的生命周期，它可以被创建，消灭，使其进入活动状态等等，这些状态之间可以通过系统调用或者进程调度的机制进行转换。下面的图展示了从交互式shell中启动&#8217;yes&#8217;这个应用程序的过程。 bash在使用fork()系统调用之后，只是在虚拟内存空间上复制一个和自己完全相同的拷贝，那么这时我们就可以得到两个bash，其中早先的那个bash进程通常被称作父进程，而其后被创建的那个进程被称作子进程；因为现在用户需要运行的是yes这个程序，其中原来的bash需要使用wait()进入等待状态以便腾出CPU占用时间来运行&#8217;yes&#8217;。在接下来的过程中，刚刚被创建的那个新的bash的副本，会使用exec()系统调用将yes这个应用程序的可执行文件映射并拷贝到内存中，通过这个方式，操作系统可以创建一个信的进程。yes这个程序比较特殊，他的作用事实上就是不断的输出&#8217;yes&#8217;这个字符串，但事实上任何应用程序都可能退出，比如我们这个时候按一下Ctrl-C，其后的动作是通过系统系统调用exit()结束进程。父进程bash在接收到子进程结束的消息后，可能又进入活动状态。 线程 进程管理部分的代码，着重可以看看进程的创建，消灭以及其他状态之间的转换是怎样实现的，此外，还有“线程”需要注意。线程在Linux中的实现和进程非常相似，可以说他是一种特殊的进程。线程的特殊之处在于多个线程之间共享相同的“进程空间”——这一点其实逻辑上很容易想清楚，就是多线程通常用来相应高并发的任务，而这些线程事实上完成的功能是一致的，他们之间不需要有区别。从“调度器”的角度来看，线程和进程是一致的。 信号 进程管理的另外一个重要内容是所谓的“信号”(signal)，它是一个简单的向进程传递非同期时间的功能。收到信号的进程可以选择通过指定的signal handler做一个动作，或者忽略这个信号，等等。收到信号的进程的行为于收到中断的内核非常相似。:)说了等于没说&#8230;&#8230; 内存管理 关于内存管理的内容的文章简直可以说在互联网上泛滥成灾。写文章的人从不同的角度对应该怎样管理内存的问题做了很多讨论。内存管理的策略也是多如牛毛，比如C++标准模板库中用free list实现的内存池等等。所以，看内存管理的话，很多大仙可以拍拍胸脯满怀信心的就把书翻过去了。但是，Linux作为一个经过实战考验的开源操作系统——事实上开源创造了更加安全可靠的操作系统，研究发现Linux2.6版本共5.7million行的源代码中，仅仅存在985个Bug；而如果将总代码量于工业界的平均水平相比，Linux中存在114,000～171,000个Bug都可以被评价为“质量不错”——它的内存管理实现可以被认为在很多地方都具有参考和借鉴的意义。就凭这一点，内存管理简直可以说是内核代码中最值得关注的部分。 Linux内存管理可以被分为两大块。第一是实内存管理，另外一个是虚拟内存的管理方法。 实内存管理 实际的内存分配策略往往区分大块内存划分和小块内存划分以提高内存分配算法的效率。Linux的实内存分配测律也不例外，她采用了以Page为单位的Buddy方法来划分大块内存区域的同时，对于小内存区块却采用了一个叫做Slab的小内存划分方法。这两者的实现都非常精巧，值得仔细研究。 虚拟内存管理 虚拟内存(Virtual Memory)技术可以说是现代计算机系统中非常重要的组成部分。它不但关系到硬件的设计，而且还关系到很多重要的软件技术的实现，比如以前文章中提到的共享库和动态链接技术。MMU(Memory Management Unit)是在计算机体系结构发展这中产生的，这个硬件组件是现代虚拟内存技术的硬件基础。Linux中采用了多重虚拟地址的虚拟内存空间，所以，它让操作系统本身获得了更加强大的能力。所以，虚拟内存管理的部分应该硬件结合软件一起来看才能看个通通透透。虚拟内存同时和内核的其他部分，比如，进程管理有很多关联的地方——比如前文提到的程序的内存映像的生成等，所以，在讨论进程的过程中也不能忘记虚拟内存。 这个部分的关键词有：Demand paging，Swapping, Page fault等等。]]></description>
			<content:encoded><![CDATA[<h3>进程管理</h3>
<h4>进程</h4>
<p><strong>进程</strong>是正在运行的程序的实体。它们是Linux用以完成各种应用程序的核心。”<a href="http://www.adamjiang.com/archives/347">链接</a>“系列文章中说明了一个应用程序如何从源代码变成可执行文件，以及如何将这个可执行文件加载进入内存从而使程序运行的过程。可执行的目标文件被加载后就行成了进程的基本组成部分。操作系统本身还会为每一个进程添加一些附加的信息用以对进程进行调度管理和创建/消灭等过程。另一方面，为了方便用户的使用，Linux向用户应用程序提供了一些系统调用帮助用户应用程序进行进程的管理。进程有自己的<em>生命周期</em>，它可以被创建，消灭，使其进入活动状态等等，这些状态之间可以通过系统调用或者进程调度的机制进行转换。下面的图展示了从交互式shell中启动&#8217;yes&#8217;这个应用程序的过程。</p>
<div class="wp-caption alignnone" style="width: 336px"><img title="从shell运行yes" src="http://farm4.static.flickr.com/3319/3230991228_1e4be053c4.jpg?v=0" alt="从shell运行yes" width="326" height="443" /><p class="wp-caption-text">从shell运行yes</p></div>
<p>bash在使用<code>fork()</code>系统调用之后，只是在虚拟内存空间上复制一个和自己完全相同的拷贝，那么这时我们就可以得到两个<em>bash</em>，其中早先的那个<em>bash</em>进程通常被称作<strong>父进程</strong>，而其后被创建的那个进程被称作<strong>子进程</strong>；因为现在用户需要运行的是<em>yes</em>这个程序，其中原来的bash需要使用<code>wait()</code>进入等待状态以便腾出CPU占用时间来运行&#8217;yes&#8217;。在接下来的过程中，刚刚被创建的那个新的<em>bash</em>的副本，会使用exec()系统调用将<em>yes</em>这个应用程序的可执行文件映射并拷贝到内存中，通过这个方式，操作系统可以创建一个信的进程。<em>yes</em>这个程序比较特殊，他的作用事实上就是不断的输出&#8217;yes&#8217;这个字符串，但事实上任何应用程序都可能退出，比如我们这个时候按一下<code>Ctrl-C</code>，其后的动作是通过系统系统调用<code>exit()</code>结束进程。父进程bash在接收到子进程结束的消息后，可能又进入活动状态。</p>
<h4>线程</h4>
<p>进程管理部分的代码，着重可以看看进程的创建，消灭以及其他状态之间的转换是怎样实现的，此外，还有“<strong>线程</strong>”需要注意。线程在Linux中的实现和进程非常相似，可以说他是一种特殊的进程。线程的特殊之处在于多个线程之间共享相同的“<strong>进程空间</strong>”——这一点其实逻辑上很容易想清楚，就是多线程通常用来相应高并发的任务，而这些线程事实上完成的功能是一致的，他们之间不需要有区别。从“调度器”的角度来看，线程和进程是一致的。</p>
<h4>信号</h4>
<p>进程管理的另外一个重要内容是所谓的“信号”(signal)，它是一个简单的向进程传递非同期时间的功能。收到信号的进程可以选择通过指定的signal handler做一个动作，或者忽略这个信号，等等。收到信号的进程的行为于收到中断的内核非常相似。:)说了等于没说&#8230;&#8230;</p>
<h3>内存管理</h3>
<p>关于内存管理的内容的文章简直可以说在互联网上泛滥成灾。写文章的人从不同的角度对应该怎样管理内存的问题做了很多讨论。内存管理的策略也是多如牛毛，比如C++标准模板库中用free list实现的内存池等等。所以，看内存管理的话，很多大仙可以拍拍胸脯满怀信心的就把书翻过去了。但是，Linux作为一个经过实战考验的开源操作系统——事实上开源创造了更加安全可靠的操作系统，研究发现<a href="http://www.wired.com/software/coolapps/news/2004/12/66022">Linux2.6版本共5.7million行的源代码中，仅仅存在985个Bug</a>；而如果将总代码量于工业界的平均水平相比，Linux中存在114,000～171,000个Bug都可以被评价为“质量不错”——它的内存管理实现可以被认为在很多地方都具有参考和借鉴的意义。就凭这一点，内存管理简直可以说是内核代码中最值得关注的部分。</p>
<p>Linux内存管理可以被分为两大块。第一是实内存管理，另外一个是虚拟内存的管理方法。</p>
<h4>实内存管理</h4>
<p>实际的内存分配策略往往区分大块内存划分和小块内存划分以提高内存分配算法的效率。Linux的实内存分配测律也不例外，她采用了以Page为单位的<strong>Buddy</strong>方法来划分大块内存区域的同时，对于小内存区块却采用了一个叫做<strong>Slab</strong>的小内存划分方法。这两者的实现都非常精巧，值得仔细研究。</p>
<h4>虚拟内存管理</h4>
<p><strong>虚拟内存</strong>(Virtual Memory)技术可以说是现代计算机系统中非常重要的组成部分。它不但关系到硬件的设计，而且还关系到很多重要的软件技术的实现，比如<a href="http://www.adamjiang.com/archives/347">以前文章</a>中提到的共享库和动态链接技术。<strong>MMU</strong>(Memory Management Unit)是在计算机体系结构发展这中产生的，这个硬件组件是现代虚拟内存技术的硬件基础。Linux中采用了多重虚拟地址的虚拟内存空间，所以，它让操作系统本身获得了更加强大的能力。所以，虚拟内存管理的部分应该硬件结合软件一起来看才能看个通通透透。虚拟内存同时和内核的其他部分，比如，进程管理有很多关联的地方——比如前文提到的程序的内存映像的生成等，所以，在讨论进程的过程中也不能忘记虚拟内存。</p>
<p>这个部分的关键词有：Demand paging，Swapping, Page fault等等。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/374/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(7)-链接4</title>
		<link>http://www.adamjiang.com/archives/347</link>
		<comments>http://www.adamjiang.com/archives/347#comments</comments>
		<pubDate>Fri, 23 Jan 2009 08:13:54 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[源代码]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=347</guid>
		<description><![CDATA[可执行目标文件 静态连接的目标输出是“可执行的目标文件”，它的基本形式当然也是ELF。只是，“可执行目标文件”与“可重定位目标文件”一些重要的区别。产生这种区别的原因在于这两种文件的目的不同。“可重定位目标文件”的目的是提供给链接器（静态链接的ld或者动态链接的ld.so）链接信息以便帮助可执行文件。而“可执行目标文件”的目的在于提供一种形式将程序的代码方便的加载到内存中以便执行。所以，“可执行的目标文件”中增加了一个叫做“段头表”(segment header table)的部分，这个表中描述了文件内容到主存的映射方法，在程序运行时，加载器(loader)将根据这个表的信息把“可执行目标文件”中的代码拷贝到主存中去。 内存布局 下面的图展示了一个典型的可执行目标文件的构成，以及在取其相应的内存“镜像”之间的简单关系（并不严格）。 +----------------------+ &#124; ELF header &#124; +----------------------+ +--------------------+ 0x00000000 &#124; segment header table &#124; &#124; NO USAGE &#124; +----------------------+ +--------------------+ 0x08048000 &#124; .init &#124; - &#62; &#124; read-only segment &#124; +----------------------+ &#124; &#124; &#124; .text &#124; - &#62; &#124; (size &#60;= 4kb*n) &#124; +----------------------+ &#124; &#124; &#124; .rodata &#124; - [...]]]></description>
			<content:encoded><![CDATA[<h3>可执行目标文件</h3>
<p>静态连接的目标输出是“可执行的目标文件”，它的基本形式当然也是ELF。只是，“可执行目标文件”与“可重定位目标文件”一些重要的区别。产生这种区别的原因在于这两种文件的目的不同。“可重定位目标文件”的目的是提供给链接器（静态链接的ld或者动态链接的ld.so）链接信息以便帮助可执行文件。而“可执行目标文件”的目的在于提供一种形式将程序的代码方便的加载到内存中以便执行。所以，“可执行的目标文件”中增加了一个叫做“段头表”(segment header table)的部分，这个表中描述了文件内容到主存的映射方法，在程序运行时，加载器(loader)将根据这个表的信息把“可执行目标文件”中的代码拷贝到主存中去。</p>
<h4>内存布局</h4>
<p>下面的图展示了一个典型的可执行目标文件的构成，以及在取其相应的内存“镜像”之间的简单关系（并不严格）。</p>
<pre class="source-code"> +----------------------+
 |    ELF header        |
 +----------------------+     +--------------------+ 0x00000000
 | segment header table |     |       NO USAGE     |
 +----------------------+     +--------------------+ 0x08048000
 |       .init          | - &gt; |  read-only segment |
 +----------------------+     |                    |
 |       .text          | - &gt; |  (size &lt;= 4kb*n)   |
 +----------------------+     |                    |
 |       .rodata        | - &gt; |  ......            |
 +----------------------+     +--------------------+
 |       .data          | - &gt; | read-write segment |
 +----------------------+     |  (size &lt;= 4kb*m)   |
 |        .bss          | - &gt; |  .....             |
 x----------------------x     +--------------------+
 /        .symtab       /     |       heap         |
 x----------------------x     +--------------------+
 /      .rel.text       /     |        ↓           |
 x----------------------x     |                    |
 /      .rel.data       /     +--------------------+ 0x40000000
 x----------------------x     |   shared library   |
 /       .debug         /     | memory map area    |
 x----------------------x     +--------------------+
 /       .line          /     |        ↓           |
 x----------------------x     |        ↑           |
 /       .strtab        /     +--------------------+
 x----------------------x     |      stack         |
 / section header table /     +--------------------+ 0xbfffffff
 x----------------------x     |      kernel        |
                              +--------------------+</pre>
<p>在IA32结构上，Linux采用<strong>虚拟内存</strong>(virtual memory)技术，所以，每个程序在内存布局的时候都好像已经拿到了所有的内存一样，而程序代码最开始的地方总是在虚拟地址<code>0x08048000</code>处。加载器从这里开始拷贝ELF中定义的只读代码，这些只读代码通常被包含在<code>.init</code><code>.text</code><code>.rodate</code>段；所谓段和节事实上同样的，只是在链接的时候，它被称为“节”，而加载时却被成为“段”。<code>.init</code>段是链接器给每个“可执行目标文件”添加的，在其中包含了程序的初始化代码的一部分；链接器在其中写入了一个叫做<code>_init</code>的函数，这个细节需要注意。只读代码要求4kb<code>对齐</code>，所以虽然它实际的大小为往往小于4kb*n，但是其后紧跟的读/写段却需要从4kb*n的虚拟内存处开始。同样，读/写段也需要4kb对齐（想想why?）。读写段之后紧跟的是堆(heap)的内存区域，众所周知，这个区域是为了malloc函数群动态分配的内存准备的；此外，这个区域将根据需要向上（向高地址区域）增长。ELF文件中的<code>.symtab</code><code>.debug</code>等内容并不会被加载进入内存，上图中用斜线表示了。操作系统还会为这个程序在内存的<code>0x40000000</code>处准备了动态加载的共享库代码区域；<code>0xbfffffff</code>处准备了另一个重要的运行时数据结构“栈”，这个区域主要用于程序中的过程调用，并且区域大小向下增长。用于应用程序的空间到这里结束了，紧跟在运行时栈的栈底后面的区域，也就是从<code>0xc0000000</code>开始就是内核代码了。</p>
<p>加载器(loader)通常是shell呼出的，但是任何应用程序都可以通过系统调用<code>execve()</code>调用加载器。</p>
<p>内存访问越界的时候通常你会被警告很奇怪的消息”segment fault”，并且程序终了。相信这个会帮助你，让你对“段”的记忆更加深刻些了，哈哈哈。这是一句很笼统的提示，但他说的就是你的指针在乱跳，可能对只读内存区域进行了写操作。</p>
<h4>启动代码</h4>
<p>加载器运行时，首先构造一个上面提到的那样的内存映像，然后根据“段头表”的指引，将程序代码拷贝到内存中。接着，他会跳转到程序的入口开始执行程序。提到c语言的程序入口，那可不就是大名鼎鼎的<code>main</code>函数么？这个说法没错，但是也不全对。真正的程序入口是一个叫做<code>_start</code>的函数，这个函数被包含在<code>crtl.o</code>文件中。这个目标文件是C语言运行时环境的一部分。它的大致的示意代码如下所示：</p>
<pre class="source-code">
0x080480c0 &lt;_start&gt;        /*  .text段的入口点                 */
  call _libc_init_first     /* 启动.text节的代码通常是初始化c的库  */
  call _init               /* 启动_init代码，也就是在_init段中   */
  call atexit              /* 注册一些在程序结束时需要作的动作     */
  call main                /* 应用程序的入口点                  */
  call _exit               /* 结束应用程序，将控制权返还给操作系统  */</pre>
<p>很明显，c语言的main函数是约定好的，如果没有这个函数程序将不能被执行。关于<code>ctrl.o</code>这个事情，让我想起面试国内某家公司的时候，曾被面试官问到这个问题；他问c程序在调用main之前需要做哪些动作，当时刚刚毕业，我的回答是现编的&#8230;&#8230;，当然是错的很离谱，恩，往事不堪回首。如果你经常看到编译时或者运行时提示找不到<code>crtl.o</code>文件，恭喜你，你可以记住它了。嘿嘿。</p>
<h3>动态链接和PIC代码</h3>
<p>顾名思义，动态链接就是将链接过程从编译时挪到了运行时。这个内容写起来会有如“懒婆娘的裹脚”，各位看官可以参照<a title="shared library on IBM developerWorks" href="http://www.ibm.com/developerworks/search/searchResults.jsp?searchType=1&amp;searchSite=dW&amp;searchScope=linuxZ&amp;query=shared+library&amp;Search=Search">IBM developerWorks的文章</a>。但是动态链接对于立志做个好程序员的有痔青年是非常重要的内容，所以，如果有时间还是要认真研究的。简单的过程应该是像这样的</p>
<h4>生成.so</h4>
<p><code><br />
gcc -shared -fPIC -o libvector.so x.c y.c z.c<br />
</code><br />
这个过程就是将源代码文件编译成为目标文件，然后在用PIC指定它进行特殊的链接重定位定位信息，其中比较重要的就是添加<strong>PLT</strong>(procedure linkage table)和<strong>GOT</strong>(global offset table)。PLT被添加到<code>.text</code>节，而GOT被添加到<code>.data</code>节。PIC代码有个缺陷，就是因为<a href="http://www.iecc.com/linker/linkerfig10-01.html">对GOT的存储器引用</a>造成的，具有大量寄存器堆的机器上没有太大问题，但是，寄存器不足的机器上却会造成严重缺陷。比如，<a href="http://sourceware.org/ml/binutils/2008-07/msg00314.html">MIPS结构的GOT问题</a>就由来已久。对于外部过程的调用，PIC代码中采用一种叫做“延迟绑定”(lazy binding)技术，这个也是需要好好学习一下的。</p>
<h4>链接共享库</h4>
<p><code><br />
gcc -o prog main.c libvector.so<br />
</code><br />
这个链接过程不像静态链接过程，链接器并不真正的拷贝共享库中的<code>.text</code>和<code>.data</code>节到可执行文件之中。相反，链接器会拷贝一些重定位和符号表信息，以便运行时可以解析对共享库代码和数据的引用。如你说知道的，这个过程就是在那个著名的$LD_LIBRARY_PATH之中去寻找共享库文件。</p>
<h4>动态加载动态链接共享库</h4>
<p><code><br />
gcc -rdynamic -o prog main.c -ldl<br />
</code><br />
通过<code>dlopen()</code>等函数可以在程序中动态地加载和链接共享库，在编译该程序时，只要连接libdl就可以了。而运行时，这个动态的加载和链接过程需要在被称为动态链接器的<code>ld.so</code>帮助下完成。这种方法特别灵活，因此被在各种各样的系统中广泛得应用，比如Java中的JNI(Java Native Interface)，通过它可以让Java程序调用本地的C或者C++函数库。</p>
<p>这篇文章的目的在于说明一个程序是怎样形成，怎样加载，最终怎样在Linux中执行的，目的已经达到了，我就不再罗嗦了。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/347/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(6)-链接3</title>
		<link>http://www.adamjiang.com/archives/323</link>
		<comments>http://www.adamjiang.com/archives/323#comments</comments>
		<pubDate>Tue, 20 Jan 2009 05:06:57 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[opensource]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[源代码]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=323</guid>
		<description><![CDATA[上一篇举例的时候那个例子并不是很恰当，因为用局部变量的生存周期来解释的话，也是行的通的。那个例子只能说是又添加了一种新的解释而已。要找一个比较妖的还挺难，就直接抄书了： /* foo5.c */ #include &#60;stdio.h&#62; void f(void); int x = 15213; int y = 15212; int main() { f(); printf("x = 0x%x y = 0x%x \n", x, y); return 0; } /* bar5.c */ double x; void f() { x = -0.0; ^^^---链接器在处理这个符号x的时候，选择了，foo5.c文件中定义的 “强符号”int型的x，也就是解释为foo5.c中的x的内存位置写入 在这里定义的double型的值。 } 在IA32/Linux机器上，double型是8个字节，而int型是4字节；因此，这里将用double型的”-0.0&#8243;覆盖foo5.c中的x和y的内存位置，于是理所当然的程序出了一个意想不到的意外，而且这类错误是不容易被发现。 静态链接库 静态链接库就是把一堆相关的.o文件使用ar工具打包。最著名的静态连接库恐怕就是libc.a了。这是C语言标准库的静态链接版本。程序跟静态连接库链接的时候一般采用如下形式的命令： $ gcc -O2 -c main.c [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.adamjiang.com/archives/308">上一篇</a>举例的时候那个例子并不是很恰当，因为用局部变量的生存周期来解释的话，也是行的通的。那个例子只能说是又添加了一种新的解释而已。要找一个比较妖的还挺难，就直接抄书了：</p>
<pre class="source-code">/* foo5.c */
#include &lt;stdio.h&gt;
void f(void);

int x = 15213;
int y = 15212;

int main()
{
	f();
	printf("x = 0x%x y = 0x%x \n",
		x, y);
	return 0;
}</pre>
<pre class="source-code">/* bar5.c */
double x;

void f()
{
	x = -0.0;
	^^^---链接器在处理这个符号x的时候，选择了，foo5.c文件中定义的
	      “强符号”int型的x，也就是解释为foo5.c中的x的内存位置写入
	      在这里定义的double型的值。
}</pre>
<p>在IA32/Linux机器上，double型是8个字节，而int型是4字节；因此，这里将用double型的”-0.0&#8243;覆盖foo5.c中的x和y的内存位置，于是理所当然的程序出了一个意想不到的意外，而且这类错误是不容易被发现。</p>
<h3>静态链接库</h3>
<p>静态链接库就是把一堆相关的<code>.o</code>文件使用<code>ar</code>工具打包。最著名的静态连接库恐怕就是<code>libc.a</code>了。这是C语言标准库的静态链接版本。程序跟静态连接库链接的时候一般采用如下形式的命令：</p>
<pre class="source-code">$ gcc -O2 -c main.c
$ gcc -static -o swap_sample main.o libswap.a</pre>
<p>程序在跟静态库链接的时候，首先链接器会按照命令行输入的从前往后的方向对可重定位文件进行符号解析，找出在模块内部未定义的符号，并将在其后找到包含这个符号定义的那个模块的代码和数据拷贝进入将生成的<strong>可执行目标文件</strong>，并对其中的符号进行<strong>重定位</strong>，如果这些未定义的符号全部解决，则链接成功并输出可执行文件，否则链接器会报错。</p>
<h4>重定位</h4>
<p>重定位就是确定一个对象（包括代码和数据）在存储器中的位置的过程。关于每个需要重定位的符号，链接器有两个方面事情要做，1. 对模块中的符号的<strong>定义</strong>(definition)进行定位，这个工作主要是合并各个输入模块的代码和数据节，并给每个节和每个符号定义赋以新的存储器地址； 2. 将模块中的<strong>引用</strong>(reference)指向正确的符号定义位置，这个工作主要依靠“重定位表目”完成，也就是<a href="http://www.adamjiang.com/archives/308">上一篇</a>中提到的实例中的<code>rel.text</code>和<code>rel.data</code>节的总览中提到的”<strong>R_386_PC32</strong>“和”<strong>R_386_32</strong>“等附带有重定位类型的表目。</p>
<p>重定位表目可以用下面的包括下面代码展示的内容：</p>
<pre class="source-code">typedef struct {
	int offset;	/* 需要被重定位的“引用”在所在节中的偏移量 */
	int symbol:24,	/* 这个引用应该指向的符号 */
	    type:8;	/* 重定位类型 */
}</pre>
<p>最重要的两类重定位类型就是”R_386_PC32&#8243;和”R_386_32&#8243;。</p>
<blockquote><p>R_386_PC32: 这个类型的重定位信息主要控制的是程序在执行是的跳转。重定位一个使用32位PC(program counter)相关的地址引用。当CPU执行使用PC相关寻址的指令时，它就将在代码中编码的32位值加上PC当前运行时的值，得到有效地址，而PC值通常默认是存储器中的下一条指令的地址。</p></blockquote>
<blockquote><p>R_386_32：重定位一个使用32位绝对地址的引用。通过绝对寻址，CPU直接使用在指令中编码的32值作为有效地址。————《深入理解计算机系统》</p></blockquote>
<p>重定位符号应用的算法伪代码如下：</p>
<pre class="source-code">foreach section s {
	foreach relocation entry r {
		refptr = s + r.offset; /* 指向需要被重定位的引用的指针 */

	/* relocate a PC-relative reference */
	if (r.type = R_386_PC32) {
		refaddr = ADDR(s) + r.offset; /* 引用的运行时地址 */
		*refptr = (unsigned) ((ADDR(r.symbol) + *refptr - refaddr);
	}

	/* relocate an absolute reference */
	if (r.type == R_386_32)
		*refptr = (unsigned) (ADDR(r.symbol) + *refptr);
	}
}</pre>
<h4>R_386_PC32</h4>
<p>在没有跳转的情况下，众所周知程序是按照从上到下的顺序顺序执行的，而这个事实在机器语言级别的直接反应就是PC的值默认情况下都会指向（经过call指令的计算后）当前执行指令的邻近下一条指令的地址。IA32结构中，一条指令的大小是4字节，所以，call指令的默认参数总是”-4&#8243;(0xfffffc)，以便操作数于PC值相加时，跳转到临近的下一条指令。也就是说上面伪代码中的refptr在PC相关的地址引用中，初始值是”-4&#8243;。那么，如果程序发生非顺序执行的跳转，其重点因素就是要给call等类似的指令一个正确的操作数。这个操作数与PC中的值进行计算之后可以跳转到相应的对象（代码）保证程序的正确执行。”R_386_PC32&#8243;这种类型的重定位过程就是给call或者类似指令计算一个正确的操作数的过程。上面展示的伪代码中的refptr就是这个操作数。因为在给符号定义(definition)定位的过程中，ADDR(r.symbol)是确定的，所以，refptr就是可以计算的。</p>
<h4>R_386_32</h4>
<p>这种情况就简单些，计算方法只是将可重定位引用所在节的首地址和其偏移量相加，这样就能确定符号在虚存中的位置。</p>
<p>未完待续。</p>
<p>&#8212;&#8212;写完后偷偷修改的分割线&#8212;&#8212;-<br />
这两天些的东西非常tmd的艰深难懂，但是硬骨头还是要啃的。市面上有很多SourceReview的书，但是读完之后总是觉得只见树木不见森林。我想要一个从上到下看到通通投投的Linux内核“解析体验”，哈哈哈。理论是比较枯燥，细节是比较烦人，但是所有奇妙的计算效果就是用这些东西为基础的，没有办法。很多时候也许真的需要不求甚解，但是，我是个偏执狂，如果遇到自己感兴趣却没有弄通的东西总觉得如鲠在喉。最终结果是写这样的文章难为自己，看这个样的文章吓走朋友，哈哈哈</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/323/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(5)-链接2</title>
		<link>http://www.adamjiang.com/archives/308</link>
		<comments>http://www.adamjiang.com/archives/308#comments</comments>
		<pubDate>Mon, 19 Jan 2009 10:15:56 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[opensource]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[源代码]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=308</guid>
		<description><![CDATA[可重定位目标文件实例解析 上回书说到ELF的文件格式，这里看一个真实的例子：用readelf工具窥看一下上篇提到的main.c编译而成的main.o文件。 $ gcc -O2 -g -c main.c -o main.o $ file swap.o swap.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped file命令的结果显示，main.o文件是一个386结构上的“可重定位目标文件”，并且包含调试信息。这个我使用的编译命令是相应的。用readelf工具读出main.o的细节看一下，将是下面这个样子。重要的部分直接插入了注解。 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 [...]]]></description>
			<content:encoded><![CDATA[<h3>可重定位目标文件实例解析</h3>
<p><a href="http://www.adamjiang.com/archives/298">上回书</a>说到ELF的文件格式，这里看一个真实的例子：用readelf工具窥看一下上篇提到的<code>main.c</code>编译而成的<code>main.o</code>文件。</p>
<pre class="source-code">$ gcc -O2 -g -c main.c -o main.o
$ file swap.o
swap.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped</pre>
<p>file命令的结果显示，<code>main.o</code>文件是一个386结构上的“可重定位目标文件”，并且包含调试信息。这个我使用的编译命令是相应的。用readelf工具读出<code>main.o</code>的细节看一下，将是下面这个样子。重要的部分直接插入了注解。</p>
<pre class="source-code">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
                                                      ^^^---<strong>小端</strong>
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
                                     ^^^---ObjectFile的类型，上篇提到的3种之一
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          900 (bytes into file)
                                     ^^^---这里指明了“节头表”的位置
  Flags:                             0x0
  Size of this header:               52 (bytes)
                                     ^^^---ELF头的大小
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
                                     ^^^---这里指明了“节头表”的大小
  Number of section headers:         23
  Section header string table index: 20</pre>
<p>上面这个就是ELF的头部信息，头部信息主要提供了ELF文件适用的体系结构以及文件各个部分的定位信息，比如“节头表”的位置/大小等。当然还包括“节头表”中记录的数量。</p>
<pre class="source-code">Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000040 000021 00  AX  0   0 16
  [ 2] .rel.text         REL             00000000 000854 000008 08     21   1  4
  [ 3] .data             PROGBITS        00000000 000064 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 00006c 000000 00  WA  0   0  4
       ^^^---未初始化的全局变量将存放在此，但仔细看他的"Size"就知道，这个节是空的；
             事实上它通常就只是一个占位符。.bss这个名字来自于IBM 704汇编语言中的
             Block Storage Start指令的首字母缩写，鉴于它只是占位符号，可以记作
             Best Save Space。（来自于《深入理解计算机系统》）恩，随你怎么叫它。

  [ 5] .debug_abbrev     PROGBITS        00000000 00006c 000060 00      0   0  1
  [ 6] .debug_info       PROGBITS        00000000 0000cc 00006a 00      0   0  1
  [ 7] .rel.debug_info   REL             00000000 00085c 000060 08     21   6  4
  [ 8] .debug_line       PROGBITS        00000000 000136 000037 00      0   0  1
  [ 9] .rel.debug_line   REL             00000000 0008bc 000008 08     21   8  4
  [10] .debug_frame      PROGBITS        00000000 000170 000050 00      0   0  4
  [11] .rel.debug_frame  REL             00000000 0008c4 000010 08     21  10  4
  [12] .debug_loc        PROGBITS        00000000 0001c0 000043 00      0   0  1
  [13] .debug_pubnames   PROGBITS        00000000 000203 000023 00      0   0  1
  [14] .rel.debug_pubnam REL             00000000 0008d4 000008 08     21  13  4
  [15] .debug_aranges    PROGBITS        00000000 000226 000020 00      0   0  1
  [16] .rel.debug_arange REL             00000000 0008dc 000010 08     21  15  4
  [17] .debug_str        PROGBITS        00000000 000246 00004c 01  MS  0   0  1
  [18] .comment          PROGBITS        00000000 000292 00002a 00      0   0  1
  [19] .note.GNU-stack   PROGBITS        00000000 0002bc 000000 00      0   0  1
       ^^^---上面是一堆调试用的二进制内容。忽略之没有什么大碍。

  [20] .shstrtab         STRTAB          00000000 0002bc 0000c5 00      0   0  1
  [21] .symtab           SYMTAB          00000000 00071c 000120 10     22  15  4
       ^^^---<strong>符号表</strong>。这位神仙是链接过程处理的重点。

  [22] .strtab           STRTAB          00000000 00083c 000016 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

There are no section groups in this file.

There are no program headers in this file.
^^^----“可重定位目标文件”并不包含program header table，这个表用于将目标文件
       映射到虚拟存储器，后文详述。</pre>
<pre class="source-code">Relocation section '.rel.text' at offset 0x854 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000012  00001002 R_386_PC32        00000000   swap
                   ^^^---符号的重定位类型，一共有11种，现在遇到的这个是最重要的两种之一的
                         “与PC相关的地址引用”

Relocation section '.rel.debug_info' at offset 0x85c contains 12 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000501 R_386_32          00000000   .debug_abbrev
                   ^^^---符号的重定位类型，最重要之二，“32位地址引用”

0000000c  00000c01 R_386_32          00000000   .debug_str
00000011  00000c01 R_386_32          00000000   .debug_str
00000015  00000c01 R_386_32          00000000   .debug_str
00000019  00000201 R_386_32          00000000   .text
0000001d  00000201 R_386_32          00000000   .text
00000021  00000701 R_386_32          00000000   .debug_line
00000027  00000c01 R_386_32          00000000   .debug_str
00000031  00000201 R_386_32          00000000   .text
00000035  00000201 R_386_32          00000000   .text
00000039  00000901 R_386_32          00000000   .debug_loc
00000065  00001101 R_386_32          00000000   buf

Relocation section '.rel.debug_line' at offset 0x8bc contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000002a  00000201 R_386_32          00000000   .text

Relocation section '.rel.debug_frame' at offset 0x8c4 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000018  00000801 R_386_32          00000000   .debug_frame
0000001c  00000201 R_386_32          00000000   .text

Relocation section '.rel.debug_pubnames' at offset 0x8d4 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000601 R_386_32          00000000   .debug_info

Relocation section '.rel.debug_aranges' at offset 0x8dc contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000601 R_386_32          00000000   .debug_info
00000010  00000201 R_386_32          00000000   .text

There are no unwind sections in this file.</pre>
<p>这一段描述了需要重定位的符号或者代码总览。其中每一个记录都描述了需要重定位的对象存在于哪一节，以及它相对于节开始位置的偏移量。</p>
<pre class="source-code">Symbol table '.symtab' contains 18 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000000     0 SECTION LOCAL  DEFAULT    6
     7: 00000000     0 SECTION LOCAL  DEFAULT    8
     8: 00000000     0 SECTION LOCAL  DEFAULT   10
     9: 00000000     0 SECTION LOCAL  DEFAULT   12
    10: 00000000     0 SECTION LOCAL  DEFAULT   13
    11: 00000000     0 SECTION LOCAL  DEFAULT   15
    12: 00000000     0 SECTION LOCAL  DEFAULT   17
    13: 00000000     0 SECTION LOCAL  DEFAULT   19
    14: 00000000     0 SECTION LOCAL  DEFAULT   18
    ^^^---符号表的前14项无须特别关心，因为他们都是编译器自己加的默认值或者调试信息。

    15: 00000000    33 FUNC    GLOBAL DEFAULT    1 main
    16: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
    17: 00000000     8 OBJECT  GLOBAL DEFAULT    3 buf
    ^^^---在main.o文件中真正存在的符号。其中，第一列描述了符号的地址相对于“节”开始的位置的偏移量。
          第二列说明的是这个对象的大小，第三列是对象类型，Ndx指明了对象存在在哪个节中，其中"1"是
          .text节，"3"是.data节。如果是"UND"，它指的是没有在这个ObjectFile中定义的符号，是需
          要重定位的对象。

No version information found in this file.</pre>
<p>谢谢各位看官的耐心，这玩意看一遍赶紧忘了吧，如果记住就是把大脑当硬盘用了。之所以细节到这个地步，只为了说明一个问题“万事万物都是有原因的”，只要仔细一点都能弄清楚事情到底怎么了，而这种仔细和耐心正是现在国内很多程序员缺少的。我参加过的与中国工程师打交道的软件项目大都让我感慨万千，中国人做事的态度和日本人做事的态度简直没有办法相提并论，不知道什么原因让很多程序员都浮躁的烧到不行&#8230;&#8230;最近参加了一些中文的邮件列表，发现这个风气尤为盛行，难道真的是民族性格问题？话又说回来，同样的工作，日本工程师拿着不止2倍于中国工程师的工资，好歹做事情的态度也应该对得起这份钱。</p>
<h3>符号和符号表</h3>
<p>上面提到的“符号”，就是“<strong>链接</strong>”阶段需要解决的重点问题的作业对象。链接需要解决被链接在一起的各个模块之间的符号和代码之间的联系，并重定位这些信息，以便生成的机器代码能够正确的跳转，或者正确的引用到某个变量的值。从链接器的角度看，符号可以分为三类 1.在本模块中定义，被其他模块引用的符号，这可能包括非静态(static)函数和非静态变量； 2.在其他模块中定义，本模块引用的符号，这种符号被称为外部符号(external symbol)； 3.在本模块中定义并只在本模块中引用的符号，这种符号成为本地符号(local symbol)，可能主要包含static函数和static全局变量。值得一提的是，本地符号并不等于程序的本地变量，因为众所周知，本地程序变量是在<strong>运行时栈</strong>中管理的，他们的生存周期很短，通过pop和push就能瞬间产生和消灭，无需符号表管理。</p>
<h3>链接器的符号解析规则</h3>
<p>本地符号的定义在链接过程中不会有大问题，每个本地符号都有唯一的定义；包括Java或者C++中的重载函数等，编译器会运用规则位有同名不同参数的函数各自产生一个唯一的“内藏”函数名。</p>
<p>全局符号比较麻烦，首先是要按照强弱分类，1.函数，已经初始化的全局变量是“<strong>强符号</strong>”；2.未初始化的全局变量是“<strong>弱符号</strong>”。然后是取舍规则，1.同名两强必出错，链接器报错； 2.同名强弱，肯定是选择强者； 3.同名两弱就随便取一个。注意啦，如果这个时候你还没有意识到明明规则存在的重要性的话，真是后知后觉了。此外，还有一个问题，就是为什么链接大批动态链接库时会有莫名其妙的错误？原因就是这些“潜规则”导致的错误了。比如，下面的例子：</p>
<p>文中有注释，c&amp;p注意。</p>
<pre class="source-code">
/* foo2.c */

void bar(void);

int x = 12345;
        ^^^---已经初始化的全局变量，“强”符号

int main()
{
	bar();
	printf("x = %d\n", x);
	return 0;
}</pre>
<pre class="source-code">/* bar2.c */
int x;
    ^^^---未初始化的全局变量，“弱”符号

void bar()
{
	x = 54321;
}</pre>
<p>这个程序被链接的并运行的话，其结果让程序员大跌眼睛的，x的输出居然还是12345。而实际现实中的问题比这个不知道要复杂多少倍，往往非常难于发现和排除，所以好的命名习惯真的是在体现一个程序员和一个软件开发团队的素养，而不是为了符合CodeStyle做的面子工程。如果你使用gcc作编译器的话，开启-warn-common选项，将帮助你查找重定义的错误。微软的编译器肯定也有这个选项，但是我有n多年都没给Windows写过程序了，实在是不知道。</p>
<p>未完待续。<br />
&#8212;-<br />
文中例子来自《深入理解计算机系统》</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/308/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(4)-链接1</title>
		<link>http://www.adamjiang.com/archives/298</link>
		<comments>http://www.adamjiang.com/archives/298#comments</comments>
		<pubDate>Sun, 18 Jan 2009 13:10:11 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[opensource]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[源代码]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=298</guid>
		<description><![CDATA[上周工作实在太忙，写blog的事情是一拖再拖&#8230;&#8230;本来准备好继续写Linux内核构成的，但是因为看书看的比较快，刚好看到一个比较关键的地方——程序的链接和载入，而这个部分有牵扯代码生成，机器语言编程，程序实例化和虚拟存储器很多内容，可以说是从程序员的角度理解Linux内核如何执行用户应用程序行为的核心。况且，这个部分牵扯很多不容忽视的“细节”，我害怕如果日子久了，就再也不记得这些内容，于是算是插入一篇，给自己记录下来。 什么是链接 大概教科书上都念过这样的经——源代码变成可执行文件一般要经过 1.编译预处理；2.编译；3.汇编；4.链接 这些过程。从使用TurboC那天起，我就被告知程序是这样产生的，但是直到我大学毕业以后的几年里，才真正知道这些事情都是怎么做的。而且，还是从一本卡耐基梅隆大学的本科生前导课程中学到的，由此可见中国所谓的大学误人子弟的老师的确不在少数。除了继续被愚弄几年以外，上那个大学又有什么意思？先忘掉将中国国内的计算机教育搞到本末倒置的Windows吧，看看Linux中的典型编译过程是怎样完成的。每个操作系统都提供一种编译驱动程序(compile driver)，最典型的例子就是gcc。gcc事实上不是一个单独的程序，而是一组程序的组合。Unix世界的逻辑就是将事情分解和简化然后分发给各个可以相互协作的部分完成，在这个设计思想下，Unix世界产成了很多专注做好一件事情的小程序，比如最简单的yes。然而，又为了解决各个小程序之间的协同工作，Unix工具集中又添加进很多tools driver，比如gcc，这种tools driver的设计思想有点像设计模式中提到的facade（这个词似乎是法语，注意发音），他把一些难以把握细节的小工具进行整合从而为用户提供一个简便的接口。gcc在编译程序的过程中实际上需要调用cpp,cc1,as和ld这些工具来帮助它完成工作，于是编译过程就是个RPG游戏，当然这里需要重点看看角色变换和他们的输入与产出。下面游戏开始： 故事背景 一个简单的Swap程序： /* main.c */ void swap(); int buf[2] = {1,2}; int main() { swap(); return 0; } /* swap.c */ extern int buf[]; int *bufp0 = &#038;buf[0]; int *bufp1; void swap() { int temp; bufp1 = &#038;buf[1]; temp = *bufp0; *bufp0 = *bufp1; *bufp1 = temp; [...]]]></description>
			<content:encoded><![CDATA[<p>上周工作实在太忙，写blog的事情是一拖再拖&#8230;&#8230;本来准备好继续写Linux内核构成的，但是因为看书看的比较快，刚好看到一个比较关键的地方——程序的链接和载入，而这个部分有牵扯代码生成，机器语言编程，程序实例化和虚拟存储器很多内容，可以说是从程序员的角度理解Linux内核如何执行用户应用程序行为的核心。况且，这个部分牵扯很多不容忽视的“细节”，我害怕如果日子久了，就再也不记得这些内容，于是算是插入一篇，给自己记录下来。</p>
<h3>什么是链接</h3>
<p>大概教科书上都念过这样的经——源代码变成可执行文件一般要经过 1.编译预处理；2.编译；3.汇编；4.链接 这些过程。从使用TurboC那天起，我就被告知程序是这样产生的，但是直到我大学毕业以后的几年里，才真正知道这些事情都是怎么做的。而且，还是从一本卡耐基梅隆大学的本科生前导课程中学到的，由此可见中国所谓的大学误人子弟的老师的确不在少数。除了继续被愚弄几年以外，上那个大学又有什么意思？先忘掉将中国国内的计算机教育搞到本末倒置的Windows吧，看看Linux中的典型编译过程是怎样完成的。每个操作系统都提供一种编译驱动程序(compile driver)，最典型的例子就是gcc。gcc事实上不是一个单独的程序，而是一组程序的组合。Unix世界的逻辑就是将事情分解和简化然后分发给各个可以相互协作的部分完成，在这个设计思想下，Unix世界产成了很多专注做好一件事情的小程序，比如最简单的yes。然而，又为了解决各个小程序之间的协同工作，Unix工具集中又添加进很多tools driver，比如gcc，这种tools driver的设计思想有点像设计模式中提到的facade（这个词似乎是法语，注意发音），他把一些难以把握细节的小工具进行整合从而为用户提供一个简便的接口。gcc在编译程序的过程中实际上需要调用cpp,cc1,as和ld这些工具来帮助它完成工作，于是编译过程就是个RPG游戏，当然这里需要重点看看角色变换和他们的输入与产出。下面游戏开始：</p>
<h4>故事背景</h4>
<p>一个简单的Swap程序：</p>
<pre class="source-code">
/* main.c */
void swap();

int buf[2] = {1,2};

int main()
{
	swap();
	return 0;
}
</pre>
<pre class="source-code">
/* swap.c */
extern int buf[];

int *bufp0 = &#038;buf[0];
int *bufp1;

void swap()
{
	int temp;

	bufp1 = &#038;buf[1];
	temp = *bufp0;
	*bufp0 = *bufp1;
	*bufp1 = temp;
}
</pre>
<h4>出场人物</h4>
<ol>
<li>cpp，“预处理器”</li>
<li>cc1，“c语言编译器”</li>
<li>as，“汇编器”</li>
<li>ld，本集主角，江湖人称“链接器”</li>
</ol>
<p>这些程序有的是不能被直接调用的，但有的是可以的。为了将问题简化，还是给gcc添加不同的选项作为驱动来观察比他们之间都发生了什么吧。具体什么选项呢，<code>man gcc</code>看看吧。</p>
<p><code><br />
gcc [-c|-S|-E] ... infile ...<br />
</code></p>
<p>gcc手册的第一行就告诉我们“他不是一个人”。这三个选项从后往前分别指明的就是 -E 预处理； -S 编译； -c 汇编；如果不加参数就直接将输入的源文件做到链接，默认情况是一条龙服务。下面的表格简单的列出了各个步骤的命令，输入和输出。</p>
<table>
<thead>
<tr>
<td>命令</td>
<td>角色</td>
<td>输出</td>
<td>注释</td>
</tr>
</thead>
<tbody>
<tr>
<td><code>gcc -E x</code></td>
<td><code>cpp</code></td>
<td>main.i</td>
<td>这里产生一个经过预处理的中间文件</td>
</tr>
<tr>
<td><code>gcc -S x</code></td>
<td><code>cc1</code></td>
<td>main.S</td>
<td>产生汇编语言文件</td>
</tr>
<tr>
<td><code>gcc -c x</code></td>
<td><code>as</code></td>
<td>main.o</td>
<td>产生可重定位的ObjectFile</td>
</tr>
</tbody>
</table>
<p>此外，<code>swap.c</code>的代码也可以按照上面的步骤按部就班的生成一个<code>swap.o</code>。接下来的工作就是用ld对已经生成的<code>.o</code>文件进行“链接”产生可执行文件。由此可见，</p>
<blockquote><p>
链接就是将不同部分的代码和数据收集和组合成一个单一文件的过程。这个文件可以被加载（或者被拷贝）到存储器中执行。（来自《深入理解计算机系统》）
</p></blockquote>
<p>但如果事情做到这个步，算是可以告一个段落，因为最重要的内容之一“可重定位的目标文件”(relocatable object file)已经生成了，也就是这里出现的<code>.o</code>文件。从技术上说<code>.o</code>与普通的二进制文件相比并没有什么特别之处，它就是一个在磁盘文件中的“字节序列”。但是，这个文件的重要之处在于他是类UNIX操作系统的ABI(application binary interface)的核心。</p>
<h3>ELF</h3>
<p>ELF的名字不错，elf似乎是德国神话传说中的一种精灵，恰恰也说明了ELF文件在系统中的执行就像是变魔法。跑题了。其实他是Executable and Linkable Format的缩写形式，<a href="http://en.wikipedia.org/wiki/Executable_and_Linkable_Format">wikipedia上关于elf的解释</a>包括英文在内都不甚详细，但是还是值得一读的。这种二进制文件格式广泛使用于各种计算机平台。最早的ObjectFile的格式是诞生于贝尔实验室的a.out，知道现在仍然有很多应用程序采用a.out形式运行。</p>
<blockquote><p>
这里插播一下历史消息。贝尔实验室的UNIX系统中使用a.out作为可执行文件的形式，而后在很多UNIX版本中这个二进制文件格式被大量的采用。UNIX系统发展的重要里程碑System V在诞生的时候采用COFF(common object file format)——微软这个偷学狂人在其Windows系统发展的过程中采用了COFF的一个变体作为自己的可执行文件的形式至今，称为PE(portable executable)——现代UNIX版本中大多采用了ELF代替此前比较原始的二进制形式。
</p></blockquote>
<p>伟大的系统在诞生和发展过程中总能产生一些伟大的部件，甚至有些系统本身已经不存在了，但它的思想或者某些精妙的实现却依然在其他系统中以某种形式存在。这个例子数不胜数，比如上面说到的ELF，再比如研发Plan 9操作系统(现在依然存在)的过程中诞生的unicode和procfs。</p>
<p>ObjectFile有三种形式，1.可重定位目标文件(relocatable object file)； 2.可执行目标文件(executable object file)； 3.共享目标文件(shared object file)。<br />
其中可重定位目标文件就是指<code>.o</code>文件。下面这个图展示的是一个典型的<code>.o</code>的文件组成。</p>
<pre class="source-code">
 +----------------------+
 |    ELF header        | <--帮助链接器解析ObjectFile的信息
 +----------------------+
 |       .text          | <--已编译程序的机器代码
 +----------------------+
 |       .rodata        | <--只读数据，比如pirntf的格式化字串等
 +----------------------+
 |       .data          | <--<strong>已经初始化</strong>的<strong>全局变量</strong>
 +----------------------+
 |        .bss          | <--<strong>未初始化</strong>的<strong>全局变量</strong>
 +----------------------+
 |        .symtab       | <--符号表，这个表是提供给链接器使用的，每个OjectFile
 +----------------------+
 |      .rel.text       | <--可重定位的代码
 +----------------------+
 |      .rel.data       | <--可重定位的数据
 +----------------------+
 |       .debug         | <--调试符号表
 +----------------------+
 |       .line          | <--.text节中机器指令于源程序行号之间的映射表
 +----------------------+
 |       .strtab        | <--字符串
 +----------------------+
 | section header table | <--节头表(section header table)
 +----------------------+
</pre>
<p>更加详细的图表可以在"<a href="http://www.iecc.com/linker/">Linkers and Loaders</a>"一书中看到，点<a href="http://www.iecc.com/linker/linker03-14.jpg" title="ELF file layout">这里</a>。</p>
<p>这些分段被称为“节”(section)，并且，在<code>.o</code>文件中为这些保留了一张表，称作“节头表”(section header table)。节头表描述了不同节的位置和大小，其作用有点像各个节的检索索引。这些节之中，<code>.debug</code>和<code>.line</code>节包含的是调试信息，只有gcc在使用"-g"选项时才能得到。而<code>.symtab</code>这个符号表节是每一个ObjectFile都会包含的，一些程序员错误的认为只有在使用"-g"选项时才能在ObjectFile中得到符号表。而这个<code>.symtab</code>节正是<strong>链接</strong>操作的核心。</p>
<p>预知后事如何，且听下回分解吧。下午约了师兄去游泳，4点从图书馆出来背着两块砖头一样的书就去了，被水一泡想说的东西全忘了。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/298/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(3)-代码阅读的陷阱</title>
		<link>http://www.adamjiang.com/archives/283</link>
		<comments>http://www.adamjiang.com/archives/283#comments</comments>
		<pubDate>Sun, 11 Jan 2009 07:08:05 +0000</pubDate>
		<dc:creator>jcadam</dc:creator>
				<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[opensource]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[源代码]]></category>

		<guid isPermaLink="false">http://www.adamjiang.com/?p=283</guid>
		<description><![CDATA[在前几天些的博文中大概说了一下应该真怎样索引代码来帮助阅读过程。但是那篇文章实在是粗略的很，粗略到几乎不能用的地步。于是想偷偷补充一下&#8230;&#8230; Cscope不能帮你的事情 Cscope本来是为了检索c语言的代码而设计的。这句话的意义就是，Cscope可以索引和帮助检索大部分内核源代码，但是却是有很多遗漏。被遗漏掉的索引理所当然的不能被检索到。所以，这是阅读源代码是必须注意的问题。Linux内核代码无法分析的部分形成原因在于： 汇编源码包括内迁汇编 汇编代码和C代码之间的调用关系 利用函数指针的函数调用 宏定义的“假函数” 利用宏在编译时动态生成的函数体 比如，第2点所说的情况，利用汇编语言调用c语言定义的函数，在i386结构的“调度器”dispatcher代码面就存在： 18 #define switch_to(prev,next,last) do { \ 19 unsigned long esi,edi; \ 20 asm volatile("pushfl\n\t" /* Save flags */ \ 21 "pushl %%ebp\n\t" \ 22 "movl %%esp,%0\n\t" /* save ESP */ \ 23 "movl %5,%%esp\n\t" /* restore ESP */ \ 24 "movl $1f,%1\n\t" /* save EIP */ [...]]]></description>
			<content:encoded><![CDATA[<p>在前几天些的博文中大概说了一下应该<del datetime="2009-01-13T05:59:37+00:00">真</del>怎样索引代码来帮助阅读过程。但是那篇文章实在是粗略的很，粗略到几乎不能用的地步。于是想偷偷补充一下&#8230;&#8230;</p>
<h3>Cscope不能帮你的事情</h3>
<p>Cscope本来是为了检索c语言的代码而设计的。这句话的意义就是，Cscope可以索引和帮助检索大部分内核源代码，但是却是有很多遗漏。被遗漏掉的索引理所当然的不能被检索到。所以，这是阅读源代码是必须注意的问题。Linux内核代码无法分析的部分形成原因在于：</p>
<ol>
<li>汇编源码包括内迁汇编</li>
<li>汇编代码和C代码之间的调用关系</li>
<li>利用函数指针的函数调用</li>
<li>宏定义的“假函数”</li>
<li>利用宏在编译时动态生成的函数体
</ol>
<p>比如，第2点所说的情况，利用汇编语言调用c语言定义的函数，在i386结构的“调度器”dispatcher代码面就存在：</p>
<pre class="source-code"> 18 #define switch_to(prev,next,last) do {                                  \
 19         unsigned long esi,edi;                                          \
 20         asm volatile("pushfl\n\t"               /* Save flags */        \
 21                      "pushl %%ebp\n\t"                                  \
 22                      "movl %%esp,%0\n\t"        /* save ESP */          \
 23                      "movl %5,%%esp\n\t"        /* restore ESP */       \
 24                      "movl $1f,%1\n\t"          /* save EIP */          \
 25                      "pushl %6\n\t"             /* restore EIP */       \
 26                      "jmp __switch_to\n"                                \
                          ^^^----------<strong>这里利用汇编代码跳转到__switch_to函数</strong>
 27                      "1:\t"                                             \
 28                      "popl %%ebp\n\t"                                   \
 29                      "popfl"                                            \
 30                      :"=m" (prev-&gt;thread.esp),"=m" (prev-&gt;thread.eip),  \
 31                       "=a" (last),"=S" (esi),"=D" (edi)                 \
 32                      :"m" (next-&gt;thread.esp),"m" (next-&gt;thread.eip),    \
 33                       "2" (prev), "d" (next));                          \
 34 } while (0)</pre>
<p>第4点中提到的宏定义函数这种情况也比较常见，比如：</p>
<pre class="source-code">#define page_buffers(page)					\
	({							\
		BUG_ON(!PagePrivate(page));			\
		((struct buffer_head *)page_private(page));	\
	})</pre>
<p>在这里利用宏定义事实上定义了一个函数，而代码索引工具是无法发现这个符号的。<br />
第5点，利用宏定义动态生成的函数体，也是无法被代码索引工具识别的。比如：</p>
<pre class="source-code"> 82 #define BUFFER_FNS(bit, name)                                           \
...
 91 static inline int buffer_##name(const struct buffer_head *bh)           \
 92 {                                                                       \
 93         return test_bit(BH_##bit, &amp;(bh)-&gt;b_state);                      \
 94 }
...
130 BUFFER_FNS(Unwritten, unwritten)</pre>
<p>在编译预处理阶段，##name将被替换为正确的缓冲名字，但是，源代码解析工具却不能看到这种变化。这类函数通常和是一些小的内嵌函数，即使是用kgdb调试的时候也是不容易被发现的，要精确发现它们只能靠你的大脑和字符搜索了。所以，在源代码阅读的之前，你必须很清楚的知道，自己究竟可能漏掉什么东西。</p>
<h3>内核源码中的其他容易被遗漏的重要内容</h3>
<p>除了上面提到的可能被源码索引工具漏掉的内容之外，还必须对下面这些内容保持警惕。<br />
<code><br />
% export LNX=${WHERE YOUR KERNEL SRC LOCATED}<br />
</code></p>
<h4>内核的链接器脚本(linker scripts)</h4>
<p><code><br />
find $LNX -name "*lds"<br />
</code><br />
你可以在<a title="链接器脚本" href="http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/gnu-linker/scripts.html">这里</a>找到<a href="http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/gnu-linker/scripts.html">链接器脚本</a>的说明。</p>
<blockquote><p>Every link is controlled by a linker script. This script is written in the linker command language.</p>
<p>The main purpose of the linker script is to describe how the sections in the input files should be mapped into the output file, and to control the memory layout of the output file. Most linker scripts do nothing more than this.</p></blockquote>
<h4>内核中重要的宏定义文件</h4>
<p><code><br />
$LNX/include/linux/moduleparam.h<br />
$LNX/include/linux/init.h<br />
</code><br />
这两个头文件中定义的宏在很多情况吓都可能被用到（特别是驱动程序开发），所以，尽可能把大脑当硬盘用一下。</p>
<h4>内核中的汇编文件</h4>
<p><code><br />
find $LNX -name "*.S"<br />
</code></p>
<h4>内核中的Makefile</h4>
<p><code><br />
find $LNX -name "Makefile"<br />
</code></p>
<h4>内核中的配置文件</h4>
<p><code><br />
find $LNX -name "*config*"<br />
</code></p>
<p>可能妨碍你理解内核行为的另外一个因素是内核设计的思想以及可能被它影响到的代码运行形式——也就是说虽然内核是使用c语言代码写的，但是并不证明她就是简单的结构化的顺序构成。这主要包含两个方面，1. 面向对象的部分实现，比如文件系统和<strong>设备模型</strong>——设备模型是内核2.5开发时的一个重要目标，目的在于统一驱动程序的动作以及分离procfs和sysfs；2. “发布——订阅”模型，比如 notification chain，主要思想类似于Design Pattern中提到的<a title="Observer Pattern" href="http://en.wikipedia.org/wiki/Observer_pattern">Observer Pattern</a>，用于事件消息的传递过程。</p>
<p>OK.源码阅读是件不容易的事情，特别是像Linux内核这样规模庞大的源代码。所以，“长期奋战在”Linxu内核开发“第一线的广大”内核开发者，很难有人说清楚究竟应该怎样去读。但是，注意上面提到的陷阱尽管去读也就是了。 :)</p>
<p>附送<a title="cscope使用的db" href="http://www.adamjiang.com/resources/make_kernel_cscope_db.sh">用来生成cscope使用db的脚本</a>。Happy hacking!</p>
<pre class="source-code">#!/bin/sh
if [ "$1" = "" ]
then
	echo "Usage: `basename $0`(linux-src-dir) (architecture)";
	echo "e.g. `basename $0` ~/linux-src/ i386";
	exit;
fi

if [ "$2" = "" ]
then
	echo "Usage: `basename $0`(linux-src-dir) (architecture)";
	echo "e.g. `basename $0` ~/linux-src/ i386";
	exit;
fi

LNX=$1
ARCH=$2
mkdir -p ${HOME}/.cscope/
cd /
find $LNX 								\
	-path "$LNX/arch/*" ! -path "$LNX/arch/${ARCH}*" -prune -o 	\
	-path "$LNX/include/asm-*" ! -path "$LNX/include/asm-${ARCH}*" -prune -o \
	-path "$LNX/tmp*" -prune -o 					\
	-path "$LNX/Documentation*" -prune -o 				\
	-path "$LNX/scripts*" -prune -o 				\
	-path "$LNX/drivers*" -prune -o 				\
	-name "*.[chxsS]" -print &gt; ${HOME}/.cscope/cscope.files

cd ${HOME}/.cscope/ #the directory with 'cscope.files'
cscope -b -q -k</pre>
</ol>
</li>
</ol>
<p>&#8212;-<br />
参考文献：<br />
<a href="http://wiki.zh-kernel.org/sniper#x86%E8%99%9A%E6%8B%9F%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E7%9A%84%E5%BB%BA%E7%AB%8B">x86虚拟调试环境的建立</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/283/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>

