<?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; 内核</title>
	<atom:link href="http://www.adamjiang.com/archives/tag/%e5%86%85%e6%a0%b8/feed" rel="self" type="application/rss+xml" />
	<link>http://www.adamjiang.com</link>
	<description>长脑袋的个人博客</description>
	<lastBuildDate>Wed, 28 Dec 2011 14:18:32 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3</generator>
		<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>
		<item>
		<title>Linux内核源码阅读系列(2)-内核的构成 之 一</title>
		<link>http://www.adamjiang.com/archives/263</link>
		<comments>http://www.adamjiang.com/archives/263#comments</comments>
		<pubDate>Thu, 08 Jan 2009 15:54:48 +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=263</guid>
		<description><![CDATA[“Linux系统”，通常我们都这样叫它。但是，这个说法简便的同时却缺少了一个重要的信息和概念。很多Linux发行版本都有自己特别的名字，但事实上他们都应该有一个共同的名字，那就是“GNU Linux”。这个前缀很重要，它表明了Linux操作系统的构成。一个是Linux内核，另外一个是主要由GNU组织提供的各种基础应用程序。而这个系列的文章里，我的讨论对象仅仅是“Linux内核”。事实上Linux内核几乎就是操作系统，如果再给它添加一个shell用来提供用户操作接口的话。操作系统是提供一个用户概念里的计算机的软件。它最基础的功能是提供用户和计算机硬件之件的抽象——这件事情Linux内核+shell就能够完成。 Linux内核－她的芳名 Linux内核是模仿传统的UNIX内核创建的，整个内核是一个非常大的程序，并非多个程序的组合，所以，Linux内核通常被成为“宏内核”。又因为她的构成方法深受传统的学院派UNIX内核的影响——特别是Minix（注1）——所以，又被称为”Monolothic kernel”。与Linux内核组成方式相对的是GNU Hurd内核，它是GNU组织计划中的尚未长大的婴儿。Hurd中以一个叫做”March”的部件为核心将n个程序组合起来的方式构成内核。以这种方式组成的内核被称为“微内核”。 另一方面，Linux内核采用了模块化的方式组织自身，所有，Linux内核可以通过编译选项方便的进行裁减形成一个体积微小的操作系统内核，某些时候她可能会被叫做“微（小）内核”，总之，这个意义不要搞混阿，这里说的仅仅是她的苗条身段罢了。 微软的Windows操作系统是“微内核”，而由BSD UNIX发展而来的Apple公司的Mac OS则很显然是宏内核。 内核代码－你不可能对她一见钟情 Linux0.0.1的代码不过才6000-8000行，互联网的上的很多说法是说，这个版本的内核代码比较适合初学者阅读。但我的想法却恰恰与此相反，Linux0.0.1代码我记得在大学3年时候曾经翻看过，但是在此以后就没有什么印象了，因为我根本用不着它。反而是工作以后，整天跟Linux2.6内核接触，却一天天的熟悉起来。由此说明，数量的多少并不是决定一个事情难易程度的主要因素，关键看自己努力与否。hoho，大嘴大话。总之，虽然Linux2.6.18内核的代码已经庞大到吓人，但是我还是觉得读这个版本的源代码对实践有真正的意义。虽然整体很大，但是化整为零的方法开始吧。解开她的第一颗钮扣之前，你得先得到她。www.kernel.org提供了一个mirror列表，你可以选择距离你比较近的下载——如果能找到邻家女孩又何必舍近求远呢？如果你在中国国内，直接使用下面的命令就可以了。注意，给wget设置proxy这个事情不在我的博客里。:) $ wget http://www.cn.kernel.org/pub/linux/kernel/v2.6/linux-2.6.18.8.tar.bz2 解压，并将她放入你自己喜欢的目录比如~/workspace/kernel-src/。掀起盖头看一下吧。大概应该是长这个样子： adam@capricorn-x61:~/workspace/kernel-src/linux-2.6.18.8$ ls COPYING MAINTAINERS arch fs kernel scripts CREDITS Makefile block include lib security Documentation README crypto init mm sound Kbuild REPORTING-BUGS drivers ipc net usr kernel Linux内核的动作控制，基本功能 mm 内存管理（物理内存，虚拟内存） fs 虚拟文件系统（VFS），各个子目录下面是各种文件系统 net 网络协议，各个子目录下面是各种协议 ipc System V的进程间通讯（IPC）机制（共享内存，信号量，消息） init 各种Linux启动时需要的代码，初始化代码 [...]]]></description>
			<content:encoded><![CDATA[<p>“Linux系统”，通常我们都这样叫它。但是，这个说法简便的同时却缺少了一个重要的信息和概念。很多Linux发行版本都有自己特别的名字，但事实上他们都应该有一个共同的名字，那就是“GNU Linux”。这个前缀很重要，它表明了Linux操作系统的构成。一个是Linux内核，另外一个是主要由GNU组织提供的各种基础应用程序。而这个系列的文章里，我的讨论对象仅仅是“Linux内核”。事实上Linux内核几乎就是操作系统，如果再给它添加一个shell用来提供用户操作接口的话。操作系统是提供一个用户概念里的计算机的软件。它最基础的功能是提供用户和计算机硬件之件的抽象——这件事情Linux内核+shell就能够完成。</p>
<h3>Linux内核－她的芳名</h3>
<p>Linux内核是模仿传统的UNIX内核创建的，整个内核是一个非常大的程序，并非多个程序的组合，所以，Linux内核通常被成为“宏内核”。又因为她的构成方法深受传统的学院派UNIX内核的影响——特别是<a href="http://www.minix3.org/">Minix</a>（注1）——所以，又被称为”Monolothic kernel”。与Linux内核组成方式相对的是GNU Hurd内核，它是GNU组织计划中的尚未长大的婴儿。Hurd中以一个叫做”March”的部件为核心将n个程序组合起来的方式构成内核。以这种方式组成的内核被称为“微内核”。</p>
<p>另一方面，Linux内核采用了模块化的方式组织自身，所有，Linux内核可以通过编译选项方便的进行裁减形成一个体积微小的操作系统内核，某些时候她可能会被叫做“微（小）内核”，总之，这个意义不要搞混阿，这里说的仅仅是她的苗条身段罢了。</p>
<p>微软的Windows操作系统是“微内核”，而由BSD UNIX发展而来的Apple公司的Mac OS则很显然是宏内核。</p>
<h3>内核代码－你不可能对她一见钟情</h3>
<p>Linux0.0.1的代码不过才6000-8000行，互联网的上的很多说法是说，这个版本的内核代码比较适合初学者阅读。但我的想法却恰恰与此相反，Linux0.0.1代码我记得在大学3年时候曾经翻看过，但是在此以后就没有什么印象了，因为我根本用不着它。反而是工作以后，整天跟Linux2.6内核接触，却一天天的熟悉起来。由此说明，数量的多少并不是决定一个事情难易程度的主要因素，关键看自己努力与否。hoho，大嘴大话。总之，虽然Linux2.6.18内核的代码已经庞大到吓人，但是我还是觉得读这个版本的源代码对实践有真正的意义。虽然整体很大，但是化整为零的方法开始吧。解开她的第一颗钮扣之前，你得先得到她。<a href="http://www.kernel.org">www.kernel.org</a>提供了一个<a href="http://www.kernel.org/mirrors/">mirror列表</a>，你可以选择距离你比较近的下载——如果能找到邻家女孩又何必舍近求远呢？如果你在中国国内，直接使用下面的命令就可以了。注意，给wget设置proxy这个事情不在我的博客里。:)<br />
<code><br />
$ wget http://www.cn.kernel.org/pub/linux/kernel/v2.6/linux-2.6.18.8.tar.bz2<br />
</code><br />
解压，并将她放入你自己喜欢的目录比如~/workspace/kernel-src/。掀起盖头看一下吧。大概应该是长这个样子：</p>
<pre>adam@capricorn-x61:~/workspace/kernel-src/linux-2.6.18.8$ ls
COPYING        MAINTAINERS     arch     fs       kernel  scripts
CREDITS        Makefile        block    include  lib     security
Documentation  README          crypto   init     mm      sound
Kbuild         REPORTING-BUGS  drivers  ipc      net     usr</pre>
<table border="0">
<tbody>
<tr>
<td>kernel</td>
<td>Linux内核的动作控制，基本功能</td>
</tr>
<tr>
<td>mm</td>
<td>内存管理（物理内存，虚拟内存）</td>
</tr>
<tr>
<td>fs</td>
<td>虚拟文件系统（VFS），各个子目录下面是各种文件系统</td>
</tr>
<tr>
<td>net</td>
<td>网络协议，各个子目录下面是各种协议</td>
</tr>
<tr>
<td>ipc</td>
<td>System V的进程间通讯（IPC）机制（共享内存，信号量，消息）</td>
</tr>
<tr>
<td>init</td>
<td>各种Linux启动时需要的代码，初始化代码</td>
</tr>
<tr>
<td>crypto</td>
<td>加密处理的共通部分</td>
</tr>
<tr>
<td>block</td>
<td>块设备的设备控制代码的共通部分</td>
</tr>
<tr>
<td>drviers</td>
<td>各种设备的驱动</td>
</tr>
<tr>
<td>sound</td>
<td>音频设备的驱动等</td>
</tr>
<tr>
<td>arch</td>
<td>各种体系结构相关的代码</td>
</tr>
<tr>
<td>include</td>
<td>Linux内核在编译的时候需要查看的头文件，开发内核模块后者设备驱动时必须参照的编程接口。其中，各个体系结构相关的代码都是以asm开头</td>
</tr>
</tbody>
</table>
<p>从上面这个表你可以看到各个子目录的大致情况，不至于需要寻找代码时无处下手。除此以外，在内核顶层目录里面你看到的各个文件“自我描述”的文件名，这个就不多说了。值得一提的是Documentation目录。Linux内核采取Bazzar方式开发，开发者的组织比较分散，所以为了保证协同工作的顺利，Linux内核的文档应该说是比较详细的，而且其中不乏幽默和智慧。但Documentation目录下的文档还是准备给用户看的，你可以在里面找到与内核开发相关的很多内容。刚刚开始的话，可以读一读<br />
<code><br />
Documentation/CodingStyle<br />
</code><br />
。另外，最近有一本被翻译成中文版就变很火的书”Dreaming in Code”——中文名《<a href="http://www.dreamingincode.cn/">梦断代码</a>》，若你真的有空，并且真的不会对内核代码一见钟情，也可以读一读。另外，若是实在闲的慌可以上<a href="http://groups.google.com/group/pongba/browse_thread/thread/c48f9b8e3e435ae1#">这个邮件列表</a>去打嘴炮。你得知道，在中国，空谈和争论是一种破文化，恩。下面有一张她的X光片。我从小在医院长大，而小时候最要好的玩伴的老爸是放射科的大夫，看胸片那是“耳濡目染”，恩。也希望你喜欢。</p>
<div class="wp-caption alignnone" style="width: 522px"><img title="Linux内核模块概览" src="http://lh5.ggpht.com/_O-FwCW3mN_w/SWRY6suuh9I/AAAAAAAADx0/4fMJ6km2Ijw/s640/Diagram1-2.jpeg" alt="Linux内核模块以及它们之间的关系" width="512" height="388" /><p class="wp-caption-text">Linux内核模块以及它们之间的关系</p></div>
<p>这张图片之中，箭头的意义可能是模块之间调用或者关联关系，但是仅仅表示这个意思，并不严格，不要较真。</p>
<h3>内核的核-她的骨架</h3>
<h4>优秀的进程调度(process scheduling)</h4>
<p>我说Linux是女人你千万别不信，由她的复杂精巧的程度就可以看出性别来。而且，她有一个非常优秀的多任务处理机制——进程调度——这就更加说明她的性别。</p>
<blockquote><p>女性的大脑结构决定了她可以一心多用：她可以同时用手玩4—5个球；她可以一边用电脑，一边接电话，同时还听别人在她身后说话，并且在整个过程中又不断地喝咖啡；她可以在谈话中同时涉及好几个毫不相关的话题，变换五种语调来转移话题或强调重点。而男人只能分辨其中的三种语调，所以男人在听女人谈话的时候总是跟不上她的思路。</p></blockquote>
<p>详细的证明过程可以参照《<a href="http://vip.book.sina.com.cn/book/index_39465.html">为什么男人爱撒谎女人爱哭泣</a>》，-_-!!!。还有就是，如果你仔细看过她的启动消息或者日记，那个verbose的喋喋不休会让你抓狂——特别是新手上路的时候，这又是一个证据。</p>
<p>要搞懂她这种古怪的行为，就必须深刻认识到“在CPU数目限制的情况下，她事实上不可能同时执行很多任务，她只能通过调度来实现看上去同时执行多个任务的假象”这一点。关于这个smart girl怎么做到这一点的，后面我想细细道来。这里需要说的是，她这一部分的故事大概都在源代码的kernel目录下。</p>
<h4>同样优秀的中断处理(interrupt handling)</h4>
<p>中断，顾名思义就是暂时停止某件事情的执行，而后，当然你还是要想办法把它做完。就像我gf下厨房，炒菜的中途发现忘记了买酱油一样，她得先关掉炉火，出去弄瓶酱油回来再继续炒菜一样，其实她大多数时候都美其名曰为了提高效率，所以，给我五毛钱打发我去买，&#8230;&#8230;，这都什么比喻。Linux中断主要有两种，一个是硬件控制产生的需要CPU给予响应的中断，另外一种是时钟中断。此外，Linux内核的中断处理设计跟我gf一样smart。她特别为了1. 提高系统响应的效率；2. 减轻系统负荷；采取了一种叫做软中断(soft interrrupt)的机制——比如，给五毛钱打发出去打酱油的时候都有一些“乌龙”软语——事实上这是一种将中断延迟处理的机制。</p>
<h4>时钟(timer)</h4>
<p>Linux自带生物钟。这个时钟主要负责两个任务，1. 时刻；2. 时限；时刻，这个很容易理解么。比如晚上9点1分才回家，她会清楚的知道，并且问你这一分钟究竟上哪里鬼混了。时限，就是你出去打酱油不能超过10分钟就得返回，因为她在家里看秒表。</p>
<h4>系统调用(system call)</h4>
<p>能够让她帮你做事的唯一交流手段。一般这里的确需要一些鲜花和巧克力，但事实上你得按照和她约定好的方式和她交流，切记“理解万岁”。Linux2.6内核中，内核向普通的用户应用程序提供2种方式的系统调用方式，一种是从Linux诞生以来就有的系统调用方式；另一种是2.6的新特性，在x86结构上可以通过sysenter命令高速执行的系统调用。事实上系统调用在内核中是以一种非常类似于中断处理的方式实现的，甚至可以说系统调用就是用户应用程序（而不是硬件）产生的中断。而系统调用的执行结果，内核通过像普通函数一样的返回值通知用户空间的应用程序。系统调用事实上也是操作系统利用CPU提供的保护模式将内核代码的执行和普通应用程序执行划分开来的结果，由此提高内核的健壮性。受保护的内核代码不会被普通应用程序直接访问，Linux在执行系统调用的时候，她会将改变CPU的执行模式，并在这个模式中执行内核代码。</p>
<h4>排他和同期(mutual exclusive &amp; synchronize)</h4>
<p>Synchronize这个词事实上原本的意义就是时间上相同，因为它含有一个&#8217;chron&#8217;词根，这个词根表示时间的意思。跑题了。Linux需要在某些情况下独占资源，又在某些情况下需要协调和同步工作，所以，内核自身必须准备一些机制以保证自身的正常运行。这些包括，1. 信号量(semaphore)；2. spin lock；3. RCU-Read Copy Update等等等。总之，这是Linux自身具有的特性，并且是基础特性。</p>
<p>基本上，在内核代码的kernel目录中你可以看到上面提到的这些内容了。因为它们组成了Linux的基础功能，所以通常被合在一起被叫做”Kernel Primitive”。装B的翻译方法通常是“内核原语”，但是我很害怕讲错，所以通常不这么叫它，再次言而总之，这个部分是你只要知道内核的核，就一切OK了。</p>
<p>－－<br />
1. Minix却是<strong>微内核</strong>的</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/263/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Linux内核源码阅读系列(1)-可能令人迷惑的C语言语法</title>
		<link>http://www.adamjiang.com/archives/251</link>
		<comments>http://www.adamjiang.com/archives/251#comments</comments>
		<pubDate>Thu, 08 Jan 2009 08:36:21 +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=251</guid>
		<description><![CDATA[Linux内核代码中使用了很多自定义的宏，如果初次开始读代码可能会感到迷惑。比如下面这个： if (unlikely(!mm)) { next->active_mm = oldmm; atomic_inc(&#038;oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next); unlikely？首先这个不是C语言的关键字，那就是宏定义了。事实上，内核代码中将likely和unlikely定义为两个帮助编译器优化的宏。宏定义如下： #define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0) __builtin_expect()宏是gcc(version>=2.9)引入的预先定义的宏，这个宏的主要作用是帮助编译器判断条件跳转的预期值。看看下面这个例子： if (__builtin_expect (x, 0)) foo (); 这里例子中，代码是在说预期x的值为“假”，并且不太期望程序执行foo()函数。而这段代码就等价于： if (unlikely(x)) foo (); 那么这个宏定义是如何影响编译器的行为的呢？看下面的一个例子（来自http://kerneltrap.org/node/4705）： [kedar@ashwamedha ~]$ cat abc.c int testfun(int x) { if(__builtin_expect(x, 0)) { ^^^--- 在这里我们告诉编译器, "else" 块的代码是我们比较期待的结果 x = 5; x = [...]]]></description>
			<content:encoded><![CDATA[<p>Linux内核代码中使用了很多自定义的宏，如果初次开始读代码可能会感到迷惑。比如下面这个：</p>
<div class="source-code">
<pre>
if (unlikely(!mm)) {
        next->active_mm = oldmm;
        atomic_inc(&#038;oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
} else
        switch_mm(oldmm, mm, next);
</pre>
</div>
<p><em>unlikely</em>？首先这个不是C语言的关键字，那就是宏定义了。事实上，内核代码中将<em>likely</em>和<em>unlikely</em>定义为两个帮助编译器优化的宏。宏定义如下：</p>
<div class="source-code">
<pre>
#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)
</pre>
</div>
<p><em>__builtin_expect()</em>宏是gcc(version>=2.9)引入的预先定义的宏，这个宏的主要作用是帮助编译器判断条件跳转的预期值。看看下面这个例子：</p>
<div class="source-code">
<pre>
if (__builtin_expect (x, 0))
       foo ();
</pre>
</div>
<p>这里例子中，代码是在说预期x的值为“假”，并且不太期望程序执行foo()函数。而这段代码就等价于：</p>
<div class="source-code">
<pre>
if (unlikely(x))
        foo ();
</pre>
</div>
<p>那么这个宏定义是如何影响编译器的行为的呢？看下面的一个例子（来自<a href="http://kerneltrap.org/node/4705">http://kerneltrap.org/node/4705</a>）：</p>
<div class="source-code">
<pre>
[kedar@ashwamedha ~]$ cat abc.c
int
testfun(int x)
{
        if(__builtin_expect(x, 0)) {
                              ^^^--- <strong>在这里我们告诉编译器, "else" 块的代码是我们比较期待的结果</strong>
                x = 5;
                x = x * x;
        } else {
                x = 6;
        }
        return x;
}

[kedar@ashwamedha ~]$ gcc -O2 -c abc.c
[kedar@ashwamedha ~]$ objdump  -d abc.o

abc.o:     file format elf32-i386

Disassembly of section .text:

00000000 :
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 45 08                mov    0x8(%ebp),%eax
   6:   85 c0                   test   %eax,%eax
   8:   75 07                   jne    11 < testfun+0x11 >
                                ^^^ --- <strong>编译器在这里为"if"块产生跳转代码但是却保持"else"块顺序执行</strong>
   a:   b8 06 00 00 00          mov    $0x6,%eax
   f:   c9                      leave
  10:   c3                      ret
  11:   b8 19 00 00 00          mov    $0x19,%eax
  16:   eb f7                   jmp    f < testfun+0xf >
</pre>
</div>
<p>如果将源代码中的<em>unlikely</em>改为<em>likely</em>，编译器生成的代码将会于上面的情况恰恰相反：</p>
<div class="source-code">
<pre>
[kedar@ashwamedha ~]$ cat abc.c
int
testfun(int x)
{
        if(__builtin_expect(x, 1)) {
                              ^^^ --- <strong>在这里我们告诉编译器, "if" 块的代码是我们比较期待的结果</strong>
                x = 5;
                x = x * x;
        } else {
                x = 6;
        }
        return x;
}

[kedar@ashwamedha ~]$ gcc -O2 -c abc.c
[kedar@ashwamedha ~]$ objdump  -d abc.o

abc.o:     file format elf32-i386

Disassembly of section .text:

00000000 :
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 45 08                mov    0x8(%ebp),%eax
   6:   85 c0                   test   %eax,%eax
   8:   74 07                   je     11 < testfun+0x11 >
                                ^^^ --- <strong>编译器在这里为"else"块产生跳转代码但是却保持"if"块顺序执行</strong>
   a:   b8 19 00 00 00          mov    $0x19,%eax
   f:   c9                      leave
  10:   c3                      ret
  11:   b8 06 00 00 00          mov    $0x6,%eax
  16:   eb f7                   jmp    f < testfun+0xf >
</pre>
</div>
<p>由此可见<em>likely</em>和<em>unlikely</em>仅仅是在帮助编译器产生更优代码，而<strong>对真值的判断没有影响</strong>。读代码的时候记住这一点就可以了。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamjiang.com/archives/251/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
	</channel>
</rss>

