<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
<channel>
<title>
<![CDATA[狂人]]></title>
 <link>
http://wangbangjie.blogcn.com</link>
<description>
<![CDATA[程序人生------Life, to be worthy of a rational being, must be always in progression.]]></description>
<managingEditor>
<![CDATA[wangbangjie]]></managingEditor>
<dc:creator>
<![CDATA[wangbangjie]]></dc:creator>
<blogcn_uid>
wangbangjie</blogcn_uid>
<blogcn_hits>
11997</blogcn_hits>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[c++之不完全索引]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102306767.shtml</link>
<description>
<![CDATA[撰文/ 曾毅 陶文
最后更新：2004年6月12日

声明：
.本文2004年5月首发于《CSDN开发高手》，版权归该杂志与《程序员》杂志社所有。杂志限于篇幅部分内容有所删节，此处版本为相对完整版本。
.本文为介绍性文章，会随笔者学习C++语言不断更新。
前言
无数次听到“我要开始学习C++!”的呐喊，无数次听到“C++太复杂了，我真的学不会”的无奈。Stan Lippman先生曾在《C++ Primer》一书中指出“C++是最为难学的高级程序设计语言之一”，人们常将“之一”去掉以表达自己对C++的敬畏。诚然，C++程序设计语言对于学习者的确有很多难以逾越的鸿沟，体系结构的庞大，应接不暇并不断扩充的特性……除此之外，参考资料之多与冗杂使它的学习者望而却步，欲求深入者苦不堪言。希望这一份不完全导引能够成为您C++学习之路上的引路灯。
撰写本文的初衷并不打算带领大家体验古老的C++历史，如果你想了解C++的历史与其前期发展中诸多技术的演变，你应当去参考Bjarne的《The Design and Evolution of C++》。当然也不打算给大家一个无所不包的宝典（并非不想：其一是因水平有限，其二无奈C++之博大精深），所给出的仅仅是一些我们认为对于想学习C++的广大读者来说最重要并且触手可及的开发与学习资源。
本文介绍并分析了一些编译器，开发环境，库，少量的书籍以及参考网站，并且尽可能尝试着给出一个利用这些资源的导引，望对如同我们一样的初学者能够有所裨益。
编译器
在C++之外的任何语言中，编译器都从来没有受到过如此之重视。因为C++是一门相当复杂的语言，所以编译器也难于构建。直到最近我们才开始能够使用上完全符合C++标准的编译器（哦，你可能会责怪那些编译器厂商不能尽早的提供符合标准的编译器，这只能怪他们各自维系着自身的一套别人不愿接受的标准）。什么？你说这无关紧要？哦，不，你所需要的是和标准化C++高度兼容的编译环境。长远来看，只有这样的编译器对C++开发人员来说才是最有意义的工具，尤其是对于程序设计语言的学习者。一至性让代码具备可移植性，并让一门语言及其库的应用更为广泛。嗯，是的，我们这里只打算介绍一些公认的优秀编译器。
Borland C++
这个是Borland C++ Builder和Borland C++ Builder X这两种开发环境的后台编译器。（哦，我之所以将之分为两种开发环境你应当能明白为什么，正如Delphi7到Delphi8的转变，是革命性的两代。）Borland C++由老牌开发工具厂商Borland倾力打造。该公司的编译器素以速度快，空间效率高著称，Borland C++ 系列编译器秉承了这个传统，属于非常优质的编译器。标准化方面早在5.5版本的编译器中对标准化C++的兼容就达到了92.73%。目前最新版本是Borland C++ Builder X中的6.0版本，官方称100%符合ANSI/ISO的C++标准以及C99标准。嗯…这正是我前面所指的“完全符合C++标准的编译器”。
Visual C++
这个正是我们熟知的Visual Studio 和 Visual Studio.net 2002, 2003以及2005 Whidbey中带的C++编译器。由Microsoft公司研制。在Visual Studio 6.0中，因为编译器有太多地方不能与后来出现的C++标准相吻合而饱受批评（想想你在使用STL的时候编译时报出的那些令人厌恶的error和warning吧）。VC++6.0对标准化C++的兼容只有83.43%。但是随着C++编译器设计大师Stanley Lippman以及诸多C++社群达人的加盟，在Visual Studio.NET 2003中，Visual C++编译器已经成为一个非常成熟可靠的C++编译器了。Dr.Dobb's Journal的评测显示Visual C++7.1对标准C++的兼容性高达98.22%，一度成为CBX之前兼容性最好的编译器。结合强大的Visual Studio.NET开发环境，是一个非常不错的选择。至于Whidbey时代的Visual C++,似乎微软所最关注的是C++/CLI……我们不想评论微软下一代的C++编译器对标准化兼容如何，但他确实越来越适合.NET (其实你和我的感觉可能是一样的，微软不应当把标准C++这块肥肉丢给Borland,然而微软可能并不这样认为)。

GNU C++
著名的开源C++编译器。是类Unix操作系统下编写C++程序的首选。特点是有非常好的移植性，你可以在非常广泛的平台上使用它，同时也是编写跨平台，嵌入式程序很好的选择。另外在符合标准这个方面一直都非常好，GCC3.3大概能够达到96.15%。但是由于其跨平台的特性，在代码尺寸速度等优化上略微差一点。
基于GNU C++的编译器有很多，比如：
l          Mingw：http://www.mingw.org/
GCC的一个Windows的移植版本（Dev-C++的后台）
l          Cygwin：http://sources.redhat.com/cygwin/
GCC的另外一个Windows移植版本是Cygwin的一部分，Cygwin是Windows下的一个Unix仿真环境。严格的说是模拟GNU的环境，这也就是"Gnu's Not Unix"要表达的意思，噢，扯远了，这并不是我们在这里关心的实质内容。  
l          Djgpp：http://www.delorie.com/djgpp/
这是GCC的DOS移植版本。
l          RSXNT：http://www.mathematik.uni-bielefeld.de/~rainer/
这是GCC的DOS和Windows移植版本。

Intel C++
著名CPU制造厂商Intel出品的编译器，Special Design for Intel x86！对于Intel x86结构的CPU经过特别的优化。在有些应用情况下，特别是数值计算等高性能应用，仅仅采用Intel的编译器编译就能大幅度的提高性能。

Digital Mars C++
网络上提供免费下载，Zortech/Symantec C++的继承者，其前身在当年惨烈的C++四国战中也是主角之一。

开发环境
开发环境对于程序员的作用不言而喻。选择自己朝夕相处的环境也不是容易的事情，特别是在IDE如此丰富的情况下。下面就是我们推荐的一些常见的C++开发环境，并没有包括一些小型的，罕见的IDE。其中任何一款都是功能丰富，可以用作日常开发使用的。对于不同层面的开发者，请参见内文关于适用对象的描述。

Visual Studio 6.0
这个虽然是Microsoft公司的老版本的开发环境，但是鉴于其后继版本Visual Studio.NET的庞大身躯，以及初学者并不那么高的功能要求，所以推荐这个开发环境给C++的初学者，供其学习C++的最基本的部分，比如C的那部分子集，当然你别指望他能够支持最新的C99标准。在日常的开发中，仍然有很多公司使用这个经典稳定的环境，比如笔者就看曾亲见有些公司将其编译器替换为GCC做手机开发之用。

Visual Studio.NET 2003
作为Microsoft公司官方正式发布的最新版本开发环境，其中有太多激动人心的功能。结合其最新的C++编译器。对于机器配置比较好的开发人员来说，使用这个开发环境将能满足其大部分的要求。这里不打算单独说Visual Studio Whidbey,虽然Visual Studio .NET 2005 - Whidbey社区预览版已经推出，但暂不是很稳定，读者可以亲身去体验。
Borland C++ Builder 6
这个并不是Borland的C++开发环境的最新版本。选择它的原因是它不是用Java写的IDE，速度比较快。它有一个很完善的GUI窗体设计器，和Delphi共用一个VCL。由于这些特点，比较适合初学者上手。但是由于其GUI的中心位置，可能不利于对于C++语言的学习。而且其为了支持VCL这个Object Pascal写的库也对C++进行了一些私有的扩充。使得人们有一个不得不接受的事实：“Borland C++ Builder 6的高手几乎都是Delphi高手”。

Borland C++ Builder X
正如前文所述，虽然版本号上和前面那个IDE非常相象，但是其实它们是完全不同的两个集成开发环境。C++Builder更多的是一个和Delphi同步的C++版本的开发环境，C++BuilderX则是完全从C++的角度思考得出的一个功能丰富的IDE。其最大的特点是跨平台，跨编译器，多种Framework的集成，并且有一个WxWindows为基础的GUI设计器。尤其是采用了纯C++来重写了整个Framework,摒弃了以前令人无奈的版本。对于C++的开发来说，从编译器，到库，到功能集成都是非常理想的。可以预见，Borland C++ Builder X 2.0很值得C++爱好者期待。唯一令人难堪之处是作为一个C++的开发工具，其IDE是用Java写的，在配置不够理想的机器上请慎重考虑再安装。

Emacs + GCC
前面讲的大部分是Windows环境下的集成开发环境。Linux上的开发者更倾向于使用Emacs来编辑C++的文件，用Makefile来命令GCC做编译。虽然看上去比较松散，但是这些东西综合起来还是一个开0发环境。如果你能够娴熟的使用这样的环境写程序，你的水平应该足够指导我们来写这篇陋文了。

Dev C++
GCC是一个很好的编译器。在Windows上的C++编译器一直和标准有着一段距离的时候，GCC就是一个让Windows下开发者流口水的编译器。Dev-C++就是能够让GCC跑在Windows下的工具，作为集成开发环境，还提供了同专业IDE相媲美的语法高亮，代码提示，调试等功能。由于使用Delphi开发，占用内存少，速度很快，比较适合轻量级的学习和使用。

Eclipse + CDT
Eclipse可是近来大名鼎鼎的开发工具。最新一期的Jolt大奖就颁给了这个杰出的神物。说其神奇是因为，它本身是用Java写的，但是拥有比一般Java写的程序快得多的速度。而且因为其基于插件组装一切的原则，使得能够有CDT这样的插件把Eclipse变成一个C/C++的开发环境。如果你一直用Eclipse写Java的程序，不妨用它体验一下C++开发的乐趣。

工具
C++的辅助工具繁多，我们分门别类的为大家作介绍：
文档类
Doxygen
参考站点：http://www.doxygen.org
    Doxygen是一种适合C风格语言（如C++、C、IDL、Java甚至包括C#和PHP）的、开放源码的、基于命令行的文档产生器。
C++2HTML
参考站点：http://www.bedaux.net/cpp2html/
把C++代码变成语法高亮的HTML
CodeColorizer
参考站点：http://www.chami.com/colorizer/
    它能把好几种语言的源代码着色为HTML
Doc-O-Matic
参考站点：http://www.doc-o-matic.com/
Doc-O_Matic为你的C/C++，C++.net，Delphi/Pascal, VB.NET，C#和Java程序或者组件产生准确的文档。Doc-O-Matic使用源代码中的符号和注释以及外部的文档文件创建与流行的文档样式一致的文档。
DocVizor
参考站点：http://www.ucancode.net/Products/DocBuilder/Features.htm
DocVizor满足了面向对象软件开发者的基本要求——它让我们能够看到C++工程中的类层次结构。DocVizor快速地产生完整可供打印的类层次结构图，包括从第三方库中来的那些类，除此之外DocVizor还能从类信息中产生HTML文件。
SourcePublisher C++
参考站点：http://www.scitools.com/sourcepublisher_c.html
给源代码产生提供快速直观的HTML报表，包括代码，类层次结构，调用和被调用树，包含和被包含树。支持多种操作系统。
Understand
参考站点：http://www.scitools.com/ucpp.html
分析任何规模的C或者C++工程，帮助我们更好的理解以及编写文档。
代码类
CC-Rider
参考站点：http://www.cc-rider.com
CC-Rider是用于C/C++程序强大的代码可视化工具，通过交互式浏览、编辑及自动文件来促进程序的维持和发展。
CodeInspect
参考站点：http://www.yokasoft.com/
一种新的C/C++代码分析工具。它检查我们的源代码找出非标准的，可能的，以及普通的错误代码。
CodeWizard
参考站点：http://www.parasoft.com
先进的C/C++源代码分析工具，使用超过500个编码规范自动化地标明危险的，但是编译器不能检查到的代码结构。
C++ Validation Test Suites
参考站点：http://www.plumhall.com/suites.html
一组用于测试编译器和库对于标准吻合程度的代码库。
CppRefactory
参考站点：http://cpptool.sourceforge.net/
         CPPRefactory是一个使得开发者能够重构他们的C++代码的程序。目的是使得C++代码的重构能够尽可能的有效率和简单。
Lzz
参考站点：http://www.lazycplusplus.com/
         Lzz是一个自动化许多C++编程中的体力活的工具。它能够节省我们许多事件并且使得编码更加有乐趣。给出一系列的声明，Lzz会给我们创建头文件和源文件。
QA C++ Generation 2000
参考站点：http://www.programmingresearch.com/solutions/qacpp.htm
它关注面向对象的C++源代码，对有关于设计，效率，可靠性，可维护性的部分提出警告信息。
s-mail project - Java to C++DOL
参考站点：http://sadlocha.strefa.pl/s-mail/ja2dol.html
把Java源代码翻译为相应的C++源代码的命令行工具。
SNIP from Cleanscape Software International
参考站点：http://www.cleanscape.net/stdprod/snip/index.html
一个填平编码和设计之间沟壑的易于使用的C++开发工具，节省大量编辑和调试的事件，它还使得开发者能够指定设计模式作为对象模型，自动从对象模型中产生C++的类。
SourceStyler C++
参考站点：http://www.ochresoftware.com/
对C/C++源代码提供完整的格式化和排版控制的工具。提供多于75个的格式化选项以及完全支持ANSI C++。

编译类
Compilercache
参考站点：http://www.erikyyy.de/compilercache/
Compilercache是一个对你的C和C++编译器的封装脚本。每次我们进行编译，封装脚本，把编译的结果放入缓存，一旦编译相同的东西，结果将从缓存中取出而不是再次编译。
Ccache
参考站点：http://ccache.samba.org/
Ccache是一个编译器缓存。它使用起来就像C/C++编译器的缓存预处理器，编译速度通常能提高普通编译过程的5~10倍。
Cmm (C++ with MultiMethods)
参考站点：http://www.op59.net/cmm/cmm-0.28/users.html
         这是一种C++语言的扩展。读入Cmm源代码输出C++的源代码，功能是对C++语言添加了对multimethod的支持。
The Frost Project
参考站点：http://frost.flewid.de/
Forst使得你能够在C++程序中像原生的C++特性一样使用multimethod以及虚函数参数。它是一个编译器的外壳。

测试和调试类
CPPUnit
         CppUnit 是个基于 LGPL 的开源项目，最初版本移植自 JUnit，是一个非常优秀的开源测试框架。CppUnit 和 JUnit 一样主要思想来源于极限编程。主要功能就是对单元测试进行管理，并可进行自动化测试。
C++Test
参考站点：http://www.parasoft.com/
         C++ Test是一个单元测试工具，它自动化了C和C++类，函数或者组件的测试。
Cantata++
参考站点：http://www.iplbath.com/products/tools/pt400.shtml
设计的目的是为了满足在合理的经济开销下使用这个工具可以让开发工程师开展单元测试和集成测试的需求.
Purify
参考站点：http://www-900.ibm.com/cn/software/rational/products/purifyplus/index.shtml
IBM Rational PurifyPlus是一套完整的运行时分析工具，旨在提高应用程序的可靠性和性能。PurifyPlus将内存错误和泄漏检测、应用程序性能描述、代码覆盖分析等功能组合在一个单一、完整的工具包中。
BoundsChecker
BoundsChecker是一个C++运行时错误检测和调试工具。它通过在Visual Studio内自动化调试过程加速开发并且缩短上市的周期。BoundsChecker提供清楚，详细的程序错误分析，许多是对C++独有的并且在static，stack和heap内存中检测和诊断错误，以及发现内存和资源的泄漏。
Insure++
参考站点：http://www.parasoft.com/
一个自动化的运行时程序测试工具，检查难以察觉的错误,如内存覆盖，内存泄漏，内存分配错误，变量初始化错误，变量定义冲突，指针错误，库错误，逻辑错误和算法错误等。
GlowCode
参考站点：http://www.glowcode.com/
         GlowCode包括内存泄漏检查，code profiler，函数调用跟踪等功能。给C++开发者提供完整的错误诊断，和运行时性能分析工具包。
Stack Spy
参考站点：http://www.imperioustech.com/
它能捕捉stack corruption, stack over run, stack overflow等有关栈的错误。

库
在C++中，库的地位是非常高的。C++之父 Bjarne Stroustrup先生多次表示了设计库来扩充功能要好过设计更多的语法的言论。现实中，C++的库门类繁多，解决的问题也是极其广泛，库从轻量级到重量级的都有。不少都是让人眼界大开，亦或是望而生叹的思维杰作。由于库的数量非常庞大，而且限于笔者水平，其中很多并不了解。所以文中所提的一些库都是比较著名的大型库。
标准库
标准库中提供了C++程序的基本设施。虽然C++标准库随着C++标准折腾了许多年，直到标准的出台才正式定型，但是在标准库的实现上却很令人欣慰得看到多种实现，并且已被实践证明为有工业级别强度的佳作。
1、   Dinkumware C++ Library
参考站点：http://www.dinkumware.com/
P.J. Plauger编写的高品质的标准库。P.J. Plauger博士是Dr. Dobb's程序设计杰出奖的获得者。其编写的库长期被Microsoft采用，并且最近Borland也取得了其OEM的license，在其C/C++的产品中采用Dinkumware的库。
2、   RogueWave Standard C++ Library
参考站点：http://www.roguewave.com/
这个库在Borland C++ Builder的早期版本中曾经被采用，后来被其他的库给替换了。笔者不推荐使用。
3、SGI STL
参考站点：http://www.roguewave.com/
SGI公司的C++标准模版库。
4、STLport
参考站点：http://www.stlport.org/
SGI STL库的跨平台可移植版本。

准标准库——Boost
Boost库是一个经过千锤百炼、可移植、提供源代码的C++库，作为标准库的后备，是C++标准化进程的发动机之一。 Boost库由C++标准委员会库工作组成员发起，在C++社区中影响甚大，其成员已近2000人。 Boost库为我们带来了最新、最酷、最实用的技术，是不折不扣的“准”标准库。
Boost中比较有名气的有这么几个库：
Regex
正则表达式库
Spirit
LL parser framework，用C++代码直接表达EBNF
Graph
图组件和算法
Lambda
在调用的地方定义短小匿名的函数对象，很实用的functional功能
concept check
检查泛型编程中的concept
Mpl
用模板实现的元编程框架
Thread
可移植的C++多线程库
Python
把C++类和函数映射到Python之中
Pool
内存池管理
smart_ptr
5个智能指针，学习智能指针必读，一份不错的参考是来自CUJ的文章：
Smart Pointers in Boost,哦，这篇文章可以查到，CUJ是提供在线浏览的。中文版见笔者在《Dr. Dobb's Journal软件研发杂志》第7辑上的译文。

Boost总体来说是实用价值很高，质量很高的库。并且由于其对跨平台的强调，对标准C++的强调，是编写平台无关，现代C++的开发者必备的工具。但是Boost中也有很多是实验性质的东西，在实际的开发中实用需要谨慎。并且很多Boost中的库功能堪称对语言功能的扩展，其构造用尽精巧的手法，不要贸然的花费时间研读。Boost另外一面，比如Graph这样的库则是具有工业强度，结构良好，非常值得研读的精品代码，并且也可以放心的在产品代码中多多利用。
参考站点：http://www.boost.org （国内镜像：http://www.c-view.org/tech/lib/boost/index.htm ）
GUI
在众多C++的库中，GUI部分的库算是比较繁荣，也比较引人注目的。在实际开发中，GUI库的选择也是非常重要的一件事情，下面我们综述一下可选择的GUI库，各自的特点以及相关工具的支持。
1、   MFC
大名鼎鼎的微软基础类库（Microsoft Foundation Class）。大凡学过VC++的人都应该知道这个库。虽然从技术角度讲，MFC是不大漂亮的，但是它构建于Windows API 之上，能够使程序员的工作更容易,编程效率高，减少了大量在建立 Windows 程序时必须编写的代码，同时它还提供了所有一般 C++ 编程的优点，例如继承和封装。MFC 编写的程序在各个版本的Windows操作系统上是可移植的，例如，在 Windows 3.1下编写的代码可以很容易地移植到 Windows NT 或 Windows 95 上。但是在最近发展以及官方支持上日渐势微。

2、   QT
参考网站：http://www.trolltech.com/
Qt是Trolltech公司的一个多平台的C++图形用户界面应用程序框架。它提供给应用程序开发者建立艺术级的图形用户界面所需的所用功能。Qt是完全面向对象的很容易扩展，并且允许真正地组件编程。自从1996年早些时候，Qt进入商业领域，它已经成为全世界范围内数千种成功的应用程序的基础。Qt也是流行的Linux桌面环境KDE 的基础，同时它还支持Windows、Macintosh、Unix/X11等多种平台。

3、WxWindows
参考网站：http://www.wxwindows.org/
跨平台的GUI库。因为其类层次极像MFC，所以有文章介绍从MFC到WxWindows的代码移植以实现跨平台的功能。通过多年的开发也是一个日趋完善的GUI库，支持同样不弱于前面两个库。并且是完全开放源代码的。新近的C++ Builder X的GUI设计器就是基于这个库的。
4、Fox 
开放源代码的GUI库。作者从自己亲身的开发经验中得出了一个理想的GUI库应该是什么样子的感受出发，从而开始了对这个库的开发。有兴趣的可以尝试一下。
参考网站：http://www.fox-toolkit.org/
5、   WTL
基于ATL的一个库。因为使用了大量ATL的轻量级手法，模板等技术，在代码尺寸，以及速度优化方面做得非常到位。主要面向的使用群体是开发COM轻量级供网络下载的可视化控件的开发者。
6、   GTK
参考网站：http://gtkmm.sourceforge.net/
GTK是一个大名鼎鼎的C的开源GUI库。在Linux世界中有Gnome这样的杀手应用。而GTK就是这个库的C++封装版本。

网络通信
ACE
参考网站：http://www.cs.wustl.edu/~schmidt/ACE.html
C++库的代表，超重量级的网络通信开发框架。ACE自适配通信环境（Adaptive Communication Environment）是可以自由使用、开放源代码的面向对象框架，在其中实现了许多用于并发通信软件的核心模式。ACE提供了一组丰富的可复用C++包装外观（Wrapper Facade）和框架组件，可跨越多种平台完成通用的通信软件任务，其中包括：事件多路分离和事件处理器分派、信号处理、服务初始化、进程间通信、共享内存管理、消息路由、分布式服务动态（重）配置、并发执行和同步，等等。
StreamModule
参考网站：http://www.omnifarious.org/StrMod/
设计用于简化编写分布式程序的库。尝试着使得编写处理异步行为的程序更容易，而不是用同步的外壳包起异步的本质。
SimpleSocket
参考网站：http://home.hetnet.nl/~lcbokkers/simsock.htm
这个类库让编写基于socket的客户/服务器程序更加容易。
A Stream Socket API for C++
参考网站：http://www.pcs.cnu.edu/~dgame/sockets/socketsC++/sockets.html
又一个对Socket的封装库。
XML
Xerces
参考网站：http://xml.apache.org/xerces-c/
Xerces-C++ 是一个非常健壮的XML解析器，它提供了验证，以及SAX和DOM API。XML验证在文档类型定义(Document Type Definition，DTD)方面有很好的支持，并且在2001年12月增加了支持W3C XML Schema 的基本完整的开放标准。
XMLBooster
参考网站：http://www.xmlbooster.com/
这个库通过产生特制的parser的办法极大的提高了XML解析的速度，并且能够产生相应的GUI程序来修改这个parser。在DOM和SAX两大主流XML解析办法之外提供了另外一个可行的解决方案。
Pull Parser
         参考网站：http://www.extreme.indiana.edu/xgws/xsoap/xpp/ 
         这个库采用pull方法的parser。在每个SAX的parser底层都有一个pull的parser，这个xpp把这层暴露出来直接给大家使用。在要充分考虑速度的时候值得尝试。
Xalan
         参考网站：http://xml.apache.org/xalan-c/
         Xalan是一个用于把XML文档转换为HTML，纯文本或者其他XML类型文档的XSLT处理器。
CMarkup
         参考网站：http://www.firstobject.com/xml.htm
         这是一种使用EDOM的XML解析器。在很多思路上面非常灵活实用。值得大家在DOM和SAX之外寻求一点灵感。
libxml++
http://libxmlplusplus.sourceforge.net/
libxml++是对著名的libxml XML解析器的C++封装版本

科学计算
Blitz++
参考网站：http://www.oonumerics.org/blitz/
Blitz++ 是一个高效率的数值计算函数库，它的设计目的是希望建立一套既具像C++ 一样方便，同时又比Fortran速度更快的数值计算环境。通常，用C++所写出的数值程序，比 Fortran慢20%左右，因此Blitz++正是要改掉这个缺点。方法是利用C++的template技术，程序执行甚至可以比Fortran更快。Blitz++目前仍在发展中，对于常见的SVD，FFTs，QMRES等常见的线性代数方法并不提供，不过使用者可以很容易地利用Blitz++所提供的函数来构建。
POOMA
参考网站：http://www.codesourcery.com/pooma/pooma
POOMA是一个免费的高性能的C++库，用于处理并行式科学计算。POOMA的面向对象设计方便了快速的程序开发，对并行机器进行了优化以达到最高的效率，方便在工业和研究环境中使用。
MTL
参考网站：http://www.osl.iu.edu/research/mtl/
Matrix Template Library(MTL)是一个高性能的泛型组件库，提供了各种格式矩阵的大量线性代数方面的功能。在某些应用使用高性能编译器的情况下，比如Intel的编译器，从产生的汇编代码可以看出其与手写几乎没有两样的效能。
CGAL
参考网站：www.cgal.org 
Computational Geometry Algorithms Library的目的是把在计算几何方面的大部分重要的解决方案和方法以C++库的形式提供给工业和学术界的用户。

游戏开发
Audio/Video 3D C++ Programming Library
参考网站：http://www.galacticasoftware.com/products/av/
***3D是一个跨平台，高性能的C++库。主要的特性是提供3D图形，声效支持（SB,以及S3M），控制接口（键盘，鼠标和遥感），XMS。
KlayGE
参考网站：http://home.g365.net/enginedev/
国内游戏开发高手自己用C++开发的游戏引擎。KlayGE是一个开放源代码、跨平台的游戏引擎，并使用Python作脚本语言。KlayGE在LGPL协议下发行。感谢龚敏敏先生为中国游戏开发事业所做出的贡献。
OGRE
参考网站：http://www.ogre3d.org
OGRE（面向对象的图形渲染引擎）是用C++开发的，使用灵活的面向对象3D引擎。它的目的是让开发者能更方便和直接地开发基于3D硬件设备的应用程序或游戏。引擎中的类库对更底层的系统库（如：Direct3D和OpenGL）的全部使用细节进行了抽象，并提供了基于现实世界对象的接口和其它类。

线程
C++ Threads
参考网站：http://threads.sourceforge.net/
这个库的目标是给程序员提供易于使用的类，这些类被继承以提供在Linux环境中很难看到的大量的线程方面的功能。
ZThreads
参考网站：http://zthread.sourceforge.net/
一个先进的面向对象，跨平台的C++线程和同步库。

序列化
s11n
参考网站：http://s11n.net/
一个基于STL的C++库，用于序列化POD，STL容器以及用户定义的类型。
Simple XML Persistence Library
参考网站：http://sxp.sourceforge.net/
这是一个把对象序列化为XML的轻量级的C++库。

字符串
C++ Str Library
参考网站：http://www.utilitycode.com/str/
操作字符串和字符的库，支持Windows和支持gcc的多种平台。提供高度优化的代码，并且支持多线程环境和Unicode，同时还有正则表达式的支持。
Common Text Transformation Library
参考网站：http://cttl.sourceforge.net/
这是一个解析和修改STL字符串的库。CTTL substring类可以用来比较，插入，替换以及用EBNF的语法进行解析。
GRETA
参考网站：http://research.microsoft.com/projects/greta/
这是由微软研究院的研究人员开发的处理正则表达式的库。在小型匹配的情况下有非常优秀的表现。

综合
P::Classes
参考网站：http://pclasses.com/
一个高度可移植的C++应用程序框架。当前关注类型和线程安全的signal/slot机制，i/o系统包括基于插件的网络协议透明的i/o架构，基于插件的应用程序消息日志框架，访问sql数据库的类等等。
ACDK - Artefaktur Component Development Kit
参考网站：http://acdk.sourceforge.net/
这是一个平台无关的C++组件框架，类似于Java或者.NET中的框架（反射机制，线程，Unicode，废料收集，I/O，网络，实用工具，XML，等等），以及对Java, Perl, Python, TCL, Lisp, COM 和 CORBA的集成。
dlib C++ library
参考网站：http://www.cis.ohio-state.edu/~kingd/dlib/
各种各样的类的一个综合。大整数，Socket，线程，GUI，容器类,以及浏览目录的API等等。
Chilkat C++ Libraries
参考网站：http://www.chilkatsoft.com/cpp_libraries.asp
这是提供zip，e-mail，编码，S/MIME，XML等方面的库。
C++ Portable Types Library (PTypes)
参考网站：http://www.melikyan.com/ptypes/
这是STL的比较简单的替代品，以及可移植的多线程和网络库。
LFC
参考网站：http://lfc.sourceforge.net/
哦，这又是一个尝试提供一切的C++库

其他库
Loki
参考网站：http://www.moderncppdesign.com/
哦，你可能抱怨我早该和Boost一起介绍它，一个实验性质的库。作者在loki中把C++模板的功能发挥到了极致。并且尝试把类似设计模式这样思想层面的东西通过库来提供。同时还提供了智能指针这样比较实用的功能。
ATL
ATL(Active Template Library)是一组小巧、高效、灵活的类，这些类为创建可互操作的COM组件提供了基本的设施。
FC++: The Functional C++ Library
这个库提供了一些函数式语言中才有的要素。属于用库来扩充语言的一个代表作。如果想要在OOP之外寻找另一分的乐趣，可以去看看函数式程序设计的世界。大师Peter Norvig在 “Teach Yourself Programming in Ten Years”一文中就将函数式语言列为至少应当学习的6类编程语言之一。
FACT!
参考网站：http://www.kfa-juelich.de/zam/FACT/start/index.html
         另外一个实现函数式语言特性的库
Crypto++
提供处理密码，消息验证，单向hash，公匙加密系统等功能的免费库。
还有很多非常激动人心或者是极其实用的C++库，限于我们的水平以及文章的篇幅不能包括进来。在对于这些已经包含近来的库的介绍中，由于并不是每一个我们都使用过，所以难免有偏颇之处，请读者见谅。
书籍
以前熊节先生曾撰文评论相对于Java程序设计语言，C++的好书多如牛毛。荣耀先生在《程序员》杂志上撰文《C++程序设计之四书五经》也将本领域内几乎所有的经典书籍作了全面的介绍,任何关于书的评论此时看来便是很多余的了。个人浅见，除非你打算以C++作为唯一兴趣或者生存之本，一般读者确实没有足够的时间和必要将20余本书籍全部阅读。更有参考价值的是荣耀先生的另一篇文章：《至少应该阅读的九本C++著作》，可以从下面的地址浏览到此文：
http://www.royaloo.com/articles/articles_2003/9CppBooks.htm
下面几本书对于走在C++初学之路上的读者是我们最愿意推荐给大家的：
《C++ Primer》
哦，也许你会抱怨我们为什么不先介绍TCPL,但对于走在学习之路上的入门者，本书内容更为全面，更为详细易懂，我们称它为“C++的超级宝典”并不过分。配有一本不错的习题解答《C++ Primer Answer Book》可以辅助你的学习之路。
《Essential C++》
如果说《C++ Primer》是C++领域的超级宝典，那么此书作为掌握C++的大局观当之无愧。正如《.NET大局观》一书能够让读者全揽.NET，本书讲述了C++中最核心的全部主题。书虽不厚，内容精炼，不失为《C++ Primer》读者茶余饭后的主题回顾之作。
《The C++ Programming Language》
Bjarne为你带来的C++教程，真正能够告诉你怎么用才叫真正的C++的唯一一本书。虽然如同“某某程序设计语言”这样的书籍会给大家一个内容全揽，入门到精通的感觉，但本书确实不太适合初学者阅读。如果你自认为是一名很有经验的C++程序员，那至少也要反复咀嚼Bjarne先生所强调的若干内容。
《Effective C++》，《More Effective C++》
是的，正如一些C++爱好者经常以读过与没有读过上述两本作品来区分你是否是C++高手。我们也极力推崇这两本著作。在各种介绍C++专家经验的书籍里面，这两本是最贴近语言本质，看后最能够有脱胎换骨感觉的书，读此书你需每日三省汝身。
技术书籍仁者见仁，过多的评论反无太多意义，由读者喜好选择最适合自己的书方为上策。
资源网站
正如我们可以通过计算机历史上的重要人物了解计算机史的发展，C++相关人物的网站也可以使我们得到最有价值的参考与借鉴，下面的人物我们认为没有介绍的必要，只因下面的人物在C++领域的地位众所周知，我们只将相关的资源进行罗列以供读者学习，他们有的工作于贝尔实验室，有的工作于知名编译器厂商，有的在不断推进语言的标准化，有的为读者撰写了多部千古奇作……
Bjarne Stroustrup  http://www.research.att.com/~bs/
Stanley B. Lippman 
http://blogs.msdn.com/slippman/
http://www.zengyihome.net/slippman/index.htm)
Scott Meyers  http://www.aristeia.com/
David Musser  http://www.cs.rpi.edu/~musser/
Bruce Eckel  http://www.bruceeckel.com
Nicolai M. Josuttis  http://www.josuttis.com/
Herb Sutter  http://www.gotw.ca/
Andrei Alexandrescu  http://www.moderncppdesign.com/
侯捷先生  http://www.jjhou.com
孟岩先生  先生繁忙于工作，痴迷于技术，暂无个人主页，关于先生的作品可以通过CSDN的专栏和侯先生的主页访问到。
荣耀先生  http://www.royaloo.com/
潘爱民先生  http://www.icst.pku.edu.cn/panaimin/pam_homepage.htm
除了上述大师的主页外，以下的综合类C++学习参考站点是我们非常愿意向大家推荐的：
CodeProject  http://www.codeproject.com
CodeGuru  http://www.codeguru.com
Dr. Dobb's Journal  http://www.ddj.com
C/C++ Users Journal  http://www.cuj.com
C维视点  http://www.c-view.org
allaboutprogram  http://www.allaboutprogram.com

其他资料
ISO IEC JTC1/SC22/WG21 - C++：标准C++的权威参考
http://anubis.dkuug.dk/jtc1/sc22/wg21/
C++ FAQ LITE — Frequently Asked Questions: 最为全面的C++FAQ
http://www.sunistudio.com/cppfaq/index.html
C/C++ 新闻组：
你不妨尝试从这里提问和回答问题，很多不错的Q&A资源......
.alt.comp.lang.learn.c-c++  
这个简单些，如果你和我一样是个菜鸟 
.comp.lang.c++.moderated
嗯，这个显然水平高一些 
.comp.std.c++
如果你需要讨论标准C++相关话题的话

不得不写的结束语
结束的时候也是总结现状，展望未来的时候。虽然C++从脱胎于C开始，一路艰难坎坷的走过来，但是无论如何C++已经取得了工业基础的地位。文章列举的大量相关资源就是最好的证明，而业界的大量用C++写成的产品代码以及大量的C++职业工程师则是最直接的证明。同时，我们可以看到各个高校的计算机专业都开设有C++这门课程，网络上对于C++的学习讨论也从来都没有停过。但是，在Java和.NET两大企业开发平台的围攻下，给人的感觉是C++越来越“不行”了。
C++在面向企业的软件开发中，在开发便捷性等方面的确要比Java和C#差很多，其中一个问题是C++语言本身比较复杂，学习曲线比较陡峭，另外一个问题是C++标准化的时间太长，丧失了很多的壮大机会，耗费了很多精力在厂商的之间的斗争上，而C++的标准库离一个完善的程序开发框架还缺少太多太多的内容，各个第三方的类库和框架又在一致性和完整性上没法和随平台提供的框架相提并论。难道C++真的要退出历史舞台了？
从C++目前的活跃程度，以及应用现状来说是完全能够肯定C++仍然是软件工业的基础，也不会退出历史舞台的。另外从Boost，Loki这些库中我们也能够看到C++的发展非常活跃，对于新技术新思维非常激进，C++仍然广泛受到关注。从ACE在高性能通信领域的应用，以及MTL这样的库在数值计算领域的出色表现，我们可以看到C++在高性能应用场合下的不可替代的作用，而嵌入式系统这样的内存受限开发平台，比如Symbian OS上，C++已经发挥着并且将发挥更大的作用。可以预见的是以后的软件无论上层的应用怎么变，它的底层核心都会是由C/C++这样的系统级软件编写的，比如Java虚拟机，.NET Framwork。因为只有这样的系统级软件才能完全彻底的发挥机器的功能。
需要看到的是两个趋势，一个趋势是C++变得更加复杂，更加学院派，通过模板等有潜力的语法因素构造越来越精巧的库成为了现代C++的热点，虽然在利用库实现新的编程范式，乃至设计模式等方面很有开创意义，也确实产生了一些能够便捷开发的工具，但是更多的是把C++变得更加强大，更加复杂，也更加难懂，似乎也更加学院派，不得不说它正在向边缘化道路发展。另一个趋势是C++在主流的企业应用开发中已经逐渐退出了，ERP这样的企业软件开发中基本上不会考虑C++，除非需要考虑性能或者和遗留代码的集成这些因素。C++退守到系统级别语言，成为软件工业的基础是大势所趋。然而反思一下，真的是退守么？自从STL出现，无数的人风起云涌的开始支持C++,他们狂呼“我看到深夜消失了，目标软件工程的出现。我看到了可维护的代码。”是的，STL在可维护性下做得如此出色。但是又怎样呢？STL为C++铺平了现代软件工程的道路，而在上层应用程序软件开发领域这块场地早不单独属于C++,很多程序设计语言都做得很出色，疯狂的支持者会毫不犹豫地说我们应当支持C++,因为它是世界上最棒的语言。而坦率地说，你的腰杆真的那么硬么？也许只是在逃避一些事实。C++是优秀的，这不可否认，STL的出现让C++一度走上了最辉煌的时刻，然而现在看来……我的一位恩师曾言：真正能够将STL应用得淋漓尽致的人很保守地说国内也不超过200人，或许不加入STL能够使C++向着它应当发展的方向发展的更好，而现在看来，C++也应当回首到真正属于他的那一片圣地上……
参考资料
本文成文时参考了以下资源：
1、《程序员》2004年2月，3月，“C++ 程序设计之四书五经” 荣耀
2、水木清华BBS C++版精华区
3、http://jjhou.csdn.net
4、http://www.royaloo.com
5、http://www.zengyihome.net
6、C/C++ 开发人员：充实您的 XML 工具箱
http://www-900.ibm.com/developerWorks/cn/xml/x-ctlbx/index.shtml

]]></description>
<pubDate>
2006-12-25 13:29:45.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102306767.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102306767.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[嵌入式系统设计师考试大纲]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102306890.shtml</link>
<description>
<![CDATA[嵌入式系统设计师考试大纲

一、考试说明

1、考试要求：
（1）掌握计算机科学基础知识；
（2）掌握嵌入式系统的硬件、软件知识；
（3）掌握嵌入式系统分析的方法；
（4）掌握嵌入式系统设计与开发的方法及步骤；
（5）掌握嵌入式系统实施的方法
（6）掌握嵌入式系统运行维护知识；
（7）了解信息化基础知识、信息技术应用的基础知识；
（8）了解信息技术标准、安全，以及有关法律法规的基本知识；
（9）了解嵌入式技术发展趋势；
（10）正确阅读和理解计算机及嵌入式领域的英文资料。


2、通过本考试的合格人员能根据项目管理和工程技术的实际要求，按照系统总体设计规格进行软、硬件设计，编写系统开发规格说明书等相应的文档；组织和指导嵌入式系统开发实施人员实现硬件电路、编写和调试程序，并对嵌入式系统硬件设备和程序进行优化和集成测试，开发出符合系统总体设计要求的高质量嵌入式系统；具有工程师的实际工作能力和业务水平。


3、本考试设置的科目包括：
（1）嵌入式系统基础知识，考试时间为150分钟，笔试，选择题；
（2）嵌入式系统设计应用技术，考试时间为150分钟，笔试，问答题。


二、考试范围


考试科目1：嵌入式系统基础知识


1.计算机科学基础
1.1数制及转换
&#8226; 二进制、八进制、十进制和十六进制等常用数制及其相互转换
1.2数据的表示
&#8226; 数的机内表示（原码、反码、补码、移码，定点和浮点，精度和溢出）
&#8226; 字符、汉字、声音、图像的编码方式
&#8226; 校验方法和校验码（奇偶校验码、海明校验码、循环冗余校验码）
1.3算术和逻辑运算
&#8226;计算机中的二进制数运算方法
&#8226;逻辑代数的基本运算和逻辑表达式的化简
1.4计算机系统结构和重要部件的基本工作原理
&#8226;CPU和存储器的组成、性能、基本工作原理
&#8226;常用I/O设备、通信设备的性能，以及基本工作原理
&#8226;I/O接口的功能、类型和特点
&#8226;虚拟存储器基本工作原理，多级存储体系
1.5安全性、可靠性与系统性能评测基础知识
&#8226;诊断与容错
&#8226;系统可靠性分析评价
&#8226;计算机系统性能评测方法


2.嵌入式系统硬件知识
   2.1数字电路和逻辑电路基础
2.1.1电气特性
2.1.2组合电路和时序电路
2.1.3总线电路与电平转换电路
2.1.4可编程逻辑器件
2.2嵌入式微处理器基础
2.2.1嵌入式微处理器体系结构
&#8226;冯.诺伊曼结构与哈佛结构
&#8226;CISC与RISC
&#8226;流水线技术
&#8226;信息存储的字节顺序（大端存储法和小端存储法）
2.2.2嵌入式系统处理器的结构和类型
&#8226;常用8位处理器的体系结构和类型
&#8226;常用16位处理器的体系结构特点
&#8226;常用32位处理器的体系结构特点
&#8226;常用DSP处理器的体系结构特点
&#8226;多核处理器的体系结构特点
2.2.3异常
&#8226; 同步异常（陷阱、故障、终止）
&#8226; 异步异常（中断）
&#8226; 可屏蔽中断、不可屏蔽中断
&#8226; 中断优先级、中断嵌套
2.3 嵌入式系统的存储体系
2.3.1存储器系统
&#8226; 存储器系统的层次结构
&#8226; 高速缓存（Cache）
&#8226; 内存管理单元（MMU）
2.3.2 ROM的种类与选型
&#8226; 常见ROM的种类
&#8226; PROM、EPROM、E2PROM型ROM的典型特征和不同点
2.3.3 Flash Memory的种类与选型
&#8226; Flash Memory的种类
&#8226; NOR和NAND型Flash Memory的典型特征和不同点
2.3.4 RAM的种类与选型
&#8226; 常见RAM的种类
&#8226; SRAM、DRAM、DDRAM、NVRAM的典型特征和不同点
2.3.5 外存
&#8226; 常见外存的种类
&#8226; 磁盘、光盘、CF、SD等的典型特征和不同点
2.4 嵌入式系统I/O接口
2.4.1 定时器和计数器基本原理与结构
2.4.2 GPIO、PWM接口基本原理与结构
2.4.3 A/D、D/A接口基本原理与结构
2.4.4键盘、显示、触摸屏接口基本与结构
2.4.5嵌入式系统音频接口
2.5嵌入系统通信及网络接口
&#8226; PCI、USB、串口、红外、并口、SPI、IIC、PCMCIA的基本原理与结构
&#8226; 以太网、CAN、WLAN、蓝牙、1394的基本原理与结构
2.6嵌入式系统电源分类及电源原理
2.7电子电路设计
2.7.1电子电路设计基础知识
&#8226; 电子电路设计原理
&#8226; 电子电路设计方法及步骤
&#8226; 电子电路设计中的可靠性知识
2.7.2 PCB设计基础知识
&#8226; PCB设计原理
&#8226; PCB设计方法及步骤
&#8226; 多层PCB设计的注意事项及布线原则
&#8226; PCB设计中的可靠性知识
2.7.3电子电路测试基础知识
&#8226; 电子电路测试原理与方法
&#8226; 硬件抗干扰测试


3. 嵌入式系统软件知识
3.1嵌入式软件基础知识
3.1.1嵌入式软件的分类（系统软件、支撑软件、应用软件）
3.1.2无操作系统支持的嵌入式软件体系结构（轮询、中断、前后台）
3.1.3有操作系统支持的嵌入式软件体系结构
3.1.4板极支持包基础知识（系统初始化、设备驱动程序）
3.1.5嵌入式中间件（GUI、数据库）
3.2 嵌入式操作系统基础知识
3.2.1嵌入式操作系统体系结构
&#8226; 单体结构、分层结构和微内核结构
3.2.2任务管理
&#8226; 多道程序技术
&#8226; 进程、线程、任务的概念
&#8226; 任务的实现（任务的层次结构、任务控制块、任务的状态及状态转换、任务队列）
&#8226; 任务调度（调度算法的性能指标、可抢占调度、不可抢占调度、先来先服务、短作业优先算法、时间片轮转算法、优先级算法）
&#8226; 实时系统及任务调度（RMS、EDF算法）
&#8226; 任务间通信（共享内存、消息、管道、信号）
&#8226; 同步与互斥（竞争条件、临界区、互斥、信号量、死锁）
3.2.3存储管理
&#8226; Flat存储管理方式
&#8226; 分区存储管理（固定分区、可变分区）
&#8226; 地址重定位（逻辑地址、物理地址、地址映射）
&#8226; 页式存储管理
&#8226; 虚拟存储技术（程序局部性原理、虚拟页式存储管理、页面置换算法、工作集模型）
3.2.4设备管理
&#8226; 设备无关性、I/O地址、I/O控制、中断处理、缓冲技术、假脱机技术）
3.2.5文件系统基础知识
&#8226; 文件和目录
&#8226; 文件的结构和组织
&#8226; 存取方法、存取控制
&#8226; 常见嵌入式文件系统（FAT、JFFS、YAFFS）
3.2.6操作系统移植基础知识
3.3 嵌入式系统程序设计
3.3.1嵌入式软件开发基础知识
3.3.2嵌入式程序设计语言
&#8226; 汇编、编译、解释系统的基础知识和基本工作原理
&#8226; 汇编语言
&#8226; 基于过程的语言（过程/函数、参数传递、全局变量、递归、动态内存分配、数据类型）
&#8226; 面向对象的语言（对象、数据抽象、继承、多态、自动内存管理）
&#8226; 各类程序设计语言的主要特点和适用情况
3.3.3嵌入式软件开发环境
&#8226; 宿主机、目标机
&#8226; 编辑器、编译器、链接器、调试器、模拟器
&#8226; 常用嵌入式开发工具（编程器、硬件仿真器、逻辑分析仪、示波器）
&#8226; 集成开发环境
&#8226; 开发辅助工具
3.3.4嵌入式软件开发
&#8226; 软件设计（模块结构设计、数据结构设计、内存布局、面向对象的分析与设计）
&#8226; 嵌入式引导程序的设计、设备驱动程序设计、内核设计、网络程序设计、应用软件设计）
&#8226; 编码（编程规范、代码审查）
&#8226; 测试（测试环境、测试用例、测试方法、测试工具）
&#8226; 下载和运行
3.3.5嵌入式应用软件移植


4.嵌入式系统的开发与维护知识
4.1系统开发过程及其项目管理
&#8226; 系统开发生命周期各阶段的目标和任务的划分方法
&#8226; 系统开发项目管理基础知识及其常用管理工具使用方法
&#8226; 主要的系统开发方法
&#8226; 系统开发工具与环境知识
4.2 系统分析基础知识
&#8226; 系统分析的目的和任务
&#8226; 系统分析方法
&#8226; 系统规格说明书的编写方法
4.3 系统设计知识
&#8226; 传统系统设计方法
&#8226; 软硬件协同设计方法
4.4 系统实施知识
&#8226; 系统架构设计
&#8226; 系统详细设计
&#8226; 系统调试技术
&#8226; 系统测试
4.5 系统维护知识
&#8226; 系统运行管理知识
&#8226; 系统维护知识
&#8226; 系统评价知识


5.安全性知识
&#8226; 安全性基本概念
&#8226; 加密与解密机制


6.标准化知识
&#8226; 标准化的概念
&#8226; 国际标准、国家标准、行业标准、企业标准基本知识
&#8226; 代码标准、文件格式标准、安全标准、软件开发规范和文档标准知识
&#8226; 标准化机构
&#8226; 嵌入式系统相关标准


7.信息化基础知识
&#8226; 信息化和信息系统基本概念
&#8226; 有关的法律、法规


8.嵌入式技术发展趋势


9.计算机专业英语
&#8226; 正确阅读和理解相关领域的英文资料


考试科目2：嵌入式系统设计应用技术


1.嵌入式系统开发过程
1.1系统需求分析方法与步骤
1.2系统设计
&#8226; 系统硬件配置
&#8226; 系统功能组成分配
&#8226; 软硬件功能的分配
&#8226; 可行性验证及设计审查
&#8226; 系统规格
&#8226; 周期，成本及工作量估计
&#8226; 开发计划
1.3软硬件协同设计
1.4硬件设计
1.5软件设计
&#8226; 软件结构
&#8226; 设计评审
&#8226; 软件详细设计
1.6系统测试
&#8226; 测试环境
&#8226; 测试计划（内容、方法、标准、过程、检验）
&#8226; 硬件测试
&#8226; 软件测试（单元测试、集成测试）
&#8226; 软硬件联合测试
&#8226; 实施测试
1.7系统评估
1.8 软件维护


2.嵌入式系统硬件设计
2.1嵌入式系统硬件基本结构
2.1.1嵌入式微处理结构与应用
2.1.2 异常及中断处理技术
2.1.3 DMA技术
2.1.4 多处理系统
&#8226; 多处理器系统特点
&#8226; 多处理器系统构建技术
2.1.5 总线架构
&#8226; 应用系统中的总线配置
2.1.6 内存种类及架构
&#8226; 存储器系统接口设计
2.1.7数字电路和逻辑电路
&#8226; 专用集成电路
&#8226; 可编程逻辑控制器件
2.2输入/输出接口设计
2.2.1 输入/输出接口
&#8226; 接口信号电平转换
&#8226; 接口驱动电路设计
2.2.2输入/输出接口应用技术
&#8226; 外围设备
&#8226; 串口通信
&#8226; 并口通信
&#8226; 模拟接口
&#8226; 通信接口设备
&#8226; 通信标准和协议
&#8226; 数据传输方式
2.3外围设备接口应用技术
2.3.1 外围存储设备
&#8226; 存储卡，记忆棒，IC卡，MMC卡，SD卡
&#8226; DVD 、CD-R 、CD-RW
2.3.2外围输入/输出设备
&#8226; 键盘，鼠标，触摸屏
&#8226; 液晶板、LED、7段数码管、蜂鸣器
2.3.3电源设计技术
2.4可靠性与安全性设计技术
2.4.1 错误检测与隔离技术
2.4.2 冗余设计
2.4.3 系统恢复设计
2.4.4 诊断技术
2.4.5常用安全标准
2.4.6 抗干扰设计
2.4.7电磁兼容设计
2.4.8系统加密


3.嵌入式系统软件设计
3.1嵌入式系统软件结构设计
3.2嵌入式操作系统应用技术
3.2.1 时间管理
&#8226; 系统时间
&#8226; 时钟中断
3.2.2内存管理
&#8226; 静态内存管理
&#8226; 动态内存管理
3.2.3任务管理和任务间的通信
&#8226; 任务间的通信机制
&#8226; 信号量
&#8226; 邮箱
&#8226; 消息队列
3.2.4异常处理
&#8226; 异常处理方法
&#8226; 中断优先级处理方法
&#8226; 系统调用
3.2.5嵌入式文件系统应用技术
3.2.6嵌入式系统图形用户接口（GUI）应用技术
3.2.7嵌入式系统数据库应用技术
3.3嵌入式软件设计技术
3.3.1汇编语言设计
&#8226; 数据类型
&#8226; 汇编语言程序结构
&#8226; 汇编语言程序设计及优化
&#8226; 子程序调用
3.3.2嵌入式C语言设计
&#8226; ANSI-C的数据类型
&#8226; C程序结构
&#8226; C语言程序设计及优化
&#8226; 程序的编译与链接
3.3.3面向对象程序设计与开发
&#8226; 面向对象的分析与设计方法UML
&#8226; 面向对象的编程语言
&#8226; 使用C++进行嵌入式系统开发
&#8226; 使用Java进行嵌入式系统开发
3.4 系统级软件设计技术
&#8226; 嵌入式系统固件与系统初始化设计
&#8226; 设备驱动程序设计
&#8226; 硬件抽象层、板级支持包设计
&#8226; 嵌入式软件的移植技术


4.嵌入式系统开发技术
4.1系统开发环境
4.1.1开发工具
&#8226; 文本编辑器
&#8226; 汇编、编译和连接程序
&#8226; ICE和ICE监控器
&#8226; 配置管理工具
&#8226; 逆向工程工具
4.1.2平台
&#8226; 操作系统
&#8226; 分布式开发环境
4.1.3开发环境创建方法及评估
&#8226; 开发工作分析
&#8226; 开发环境的建立
&#8226; 维护、管理、使用开发环境的方法
&#8226; 开发环境的评测
4.2实时系统的分析技术
4.2.1实时系统的分析技术
&#8226; 结构化分析方法
&#8226; 面向对象分析方法
4.2.2实时系统的设计技术
&#8226; 结构化设计方法
&#8226; 面向对象设计方法
4.3硬件设计环境
4.3.1硬件描述语言
&#8226; 硬件开发设计过程
&#8226; 硬件描述语言的种类与特点
4.3.2仿真技术
&#8226; 逻辑仿真方法
&#8226; 逻辑仿真工具
4.3.3大规模集成电路系统的开发方法
&#8226; ASIC开发方法
&#8226; FPGA设计方法
&#8226; IP（intellectual property）
4.4协同设计
&#8226; 软硬件任务分工和协调
&#8226; 设计评审
4.5嵌入式系统低功耗设计技术
&#8226;  低功耗系统工作机制
&#8226;  低功耗系统模型结构
&#8226;  低功耗的硬件设计技术
&#8226;  低功耗的软件设计技术
4.6分布式嵌入系统设计
&#8226;  分布式系统设计原理
&#8226; 分布式系统设计方法
&#8226;  分布式系统的通信技术
&#8226;  分布式系统设计应用


5.嵌入式系统应用
5.1嵌入式系统在控制领域中的应用
5.2嵌入式系统在手持设备中的应用
5.3嵌入式系统在模式识别中的应用]]></description>
<pubDate>
2006-10-25 08:24:48.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102306890.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102306890.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[服务器攻略]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102306171.shtml</link>
<description>
<![CDATA[http://search.chinaunix.net/cgi-bin/search?mode=all_author&u=429829&key=chenrao]]></description>
<pubDate>
2006-09-07 14:29:44.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102306171.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102306171.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[嵌入式系统 Boot Loader 技术内幕]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307091.shtml</link>
<description>
<![CDATA[http://www-128.ibm.com/developerworks/cn/linux/l-btloader/index.html
]]></description>
<pubDate>
2006-08-22 15:30:58.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307091.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307091.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[关于c99]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307296.shtml</link>
<description>
<![CDATA[Peter Seebach (developerworks@seebs.plethora.net)
自由作家
2004 年 4 月

C99 是什么？谁需要它？它可用了吗？Poter Seebach 讨论了 ISO C 标准的 1999 年修订
版，着重于 Linux 和 BSD 系统上新特性的可用性。

开放源代码操作系统所带的 gcc 发行版本并不支持 C99 的所有新特性，不过现在已经有足
够多的新特性普遍可用，因此有理由开始认真考虑在新的开发中采用 C99 特性，尤其是用
在它们使得效率和清晰度本质上发生变化的那些地方。

本文回顾了近来发布的 Linux 和 BSD 上的 C99 语言和库特性的可用性。由于这些特性很
多是 gcc 的标准特性，所以新版本的 gcc 在大部分其他平台上可以做同样的事情。当然，
各个发行版本或者各个 OS 之间的库支持是不同的。

以语言标准调用 gcc
GNU C 编译器支持许多不同版本的 C 编程语言。可以在命令行上通过 -std 选项来选择所
使用的 C 标准的版本。默认选择的不是任何版本的标准，而是"GNU C"语言，这门语言有
其自己的扩展集。 C 标准的常见版本用下面的选项选择：

C-ninety-what?
C99 标准是 ISO C 标准的最新修订版本。或许应该先介绍一些历史背景。在早期，C 语言
的开发没有组织，经历了很多变化。最后，大部分厂家都接受了 Kernighan 和 Ritchie 的
 The C Programming Language 第一版 (1978) 中描述的语言，但是扩展还是司空见惯。AN
SI 开始致力于基于此书和现有实际应用之上的标准，到 1989-1990 时，一个标准得到了广
泛的使用。这个标准就是广泛流传的"C89"；有些人戏称在 K&R 的 1978 版中描述的语言
为"C78"。在接下来的十年，编译器厂商不断开发新扩展和新特性，并在 1999 年发布了
修订的标准，这个标准描述了多年来所做的对众多最有用和广为支持的新特性所进行的标准
化工作。这个标准经常被叫做"C99"标准。


-std=c89 或 -std=iso9899:1990
最初的 C89 标准
-std=iso9899:199409
C89，增加了 Normative Addendum 1 的变化
-std=c99 or -std=iso9899:1999

C99 修订版标准
使用 -pedantic 选项来强制遵从某个版本的标准。这个选项主要用于设法确保您的代码迁
移到其他编译器时仍可用；例如，如果您正在与不使用 gcc 的人共享一个代码库 (codebas
e)，您可能希望它在任何时候都能用。注意， -pedantic 标记偶而将会得到给定的标准错
误的一些详细信息；例如，它可能试图在 C99 程序上强制执行 C89 规则，或者可能会在强
制执行模糊的规则时失败。还是值得用它来做测试。如果您正在尝试编写可移植代码，应该
好好研究一下 -std=c99 -pedantic -Wall。

C89 标准引入了一个新概念；"独立的 (freestanding)"和"托管的 (hosted)"环境之间
的区别。多数人都很熟悉托管的环境；它提供了完整的标准库，并总是从 main() 开始执行
。如果您需要独立环境所包含的稍有不同的警告与行为集合，那么使用 -ffreestanding 选
项。默认地是假定为托管的环境。为了解决常见的 FAQ，gcc 会故意在 main() 声明使用的
参数或返回类型不是标准中所列出的类型时给出警告；而 C99 标准允许实现提供另外的声
明，但是这些实现是永远不可移植的。尤其是，通常以 void 为返回类型声明 main() 的习
惯是完全错误的。(这就是 NetBSD 内核使用 -ffreestanding 标记来编译的原因。)

语言特性
C 编程语言有两个容易混淆的部分：语言和库。以前，有很多通用工具代码，大家都倾向于
重复使用，这些代码最后被标准化为标准 C 库。最初这一区别非常容易理解：如果要进行
编译，那么就是语言，如果是在附加代码中，那么就是库。事易时移，这个区别变得模糊了
。例如，一些编译器会生成对外部库的调用以进行 64 位运算，同时一些库函数可能会由编
译器不可思议地处理。本文遵循标准的术语进行区分，即来自标准的"库"部分的特性是库
特性，在文章的下一节将讨论这些特性。本节讨论除此之外的所有内容。

C99 语言引入了许多可能会引起软件开发人员兴趣的新特性。这些特性中有很多类似于 C
扩展的 GNU C 集的特性；不幸的是，在一些情况下，它们并不是很兼容。

已经增加了一些在 C++ 中常见的特性。具体来说，// 注释、混合声明和混合代码已经成为
 C99 的标准特性。这些在 GNU C 中一直都有，应该在每一个平台都可用。不过，总的来说
，C 和 C++ 仍是单独的语言；C99 与 C++ 的兼容性比 C89 要稍差一些。无论何时，试图
去编写混合的代码都不是好主意。好的 C 代码将是不好的 C++ 代码。

C99 增加了一些对 Unicode 字符（既包括字符串文字内的字符，也包括标识符内的字符）
的支持，实际上，大部分用户并不需要系统对此的支持；现在还不要期望使用这种字符的源
代码可以对其他人可用。一般而言，宽字符和 unicode 支持主要体现在编译器中，但是文
本处理工具还没有达到标准。

新的变长数组（variable-length array，VLA）已经部分可用。可以用简单的 VLA。不过，
这纯粹是一个巧合；实际上，GNU C 有其自己的变长数组支持。结果，虽然使用变长数组的
简单代码将可以工作，但是大量的代码会遇到旧的 GNU C 对 VLA 的支持与 C99 定义之间
在在差异的麻烦。可以声明长度为本地变量的数组，但是就到此为止吧。

复合文字和指定的初始化程序是非常好的代码可维护性特性。比较下面两个代码片断：

清单 1. 在 C89 中延迟 n 微秒

    /* C89 */
    {
        struct timeval tv = { 0, n };
        select(0, 0, 0, 0, &tv);
    }


清单 2. 在 C99 中延迟 n 微秒

    // C99
    select(0, 0, 0, 0, & (struct timeval) { .tv_usec = n });

 

复合文字的语法允许用大括号括起来的一系列值来初始化适当类型的自动对象。每一次运行
到对象的声明时它会被初始化，所以使用可能会修改相应对象的函数（比如一些版本的 sel
ect）时这样做是安全的。指定的初始化程序语法允许您通过名字初始化成员，不用理会它
们在对象中出现的顺序。当对象庞大而复杂却只有很少成员需要初始化时，这特别有用。使
用普通的聚合初始化程序时，缺少的值会被认为它们已经被初始化程序指定为 0 来处理。
其他初始化规则稍有修改；例如，现在您可以在 enum 声明后跟一个逗号，以更轻松地编写
代码生成器。

多年来，人们一直在争论 C 的类型系统的扩展，比如 long long。C99 引入了几个新的整
数类型。应用最广的是 long long。标准方法引入的另一种类型是 intmax_t。这两种类型
在 gcc 中都可以用。不过，整数提升规则对于比 long 更大的类型并不总是正确。可能最
好用显式的类型转换。

还有很多类型，允许对期望的性质进行更为详细的描述。例如，有的类型的名字是 int_lea
st8_t，它至少有 8 位，还有 int32_t，它恰好是 32 位。标准保证至少可以访问 8 位、1
6 位、32 位和 64 位类型。没有保证会提供精确宽度类型。不要使用这种类型，除非您肯
定是实在不能接受更大的类型。另一个可选的类型是新的 intptr_t 类型，它是一个足够大
的可以容纳一个指针的整数。并不是所有的系统都提供这样一种类型（尽管当前所有的 Lin
ux 和 BSD 实现都提供）。

C 预处理程序有很多新特性；它允许空参数，支持参数数量可变的宏，有一个用于宏生成程
序的 _Pragma 操作符，还有一个 __func__ 宏，它的内容始终是当前函数的名字。这些特
性在当前版本的 gcc 中都已经有了。

C99 增加了 inline 关键字以支持函数内联。GNU C 也支持这一关键字，但是在语义上略有
不同。如果您正在使用 gcc，而且如果您期望代码有与 C99 相同的行为，您应该记住在内
联函数前使用 static 关键字。这在以后的修订中可能会解决，同时，您可以将 inline 作
为一个编译技巧，但不要依赖于确切的语义。

C99 引入了一个限定词 restrict，它可以向编译器给出关于指针的优化提示。因为编译器
不需要对它做任何事，所以只是因为 gcc 接受了它才引入它。优化的程度不同。用它是安
全的，但是还不要希望它可以带来多大的改变。根据相关的注解，新的类型别名 (type-ali
asing) 规则已经在 gcc 中得到了完全的支持。这通常意味着您必须要更加留心类型的双义
性，它几乎总会去调用不明确的行为，除非您用来访问错误类别数据的类型是 unsigned ch
ar。

作为函数参数的数组声明符现在与指针声明符有了很大意义上的不同；您可以插入类型限定
词。尤其有意思的是给数组声明增加了 static 类型修饰符，这是非常古怪的优化提示。看
这个声明：int foo(int a[static 10]);

用一个没有指向至少 10 个 int 类型对象的指针去调用 foo() 是不明确的行为。这是一种
优化技巧。您这样做是向编译器保证传递给那个函数的参数将至少是那么大；有一些机器可
能会以此来拆解循环。老手应该会很清楚，它不是一个新的 C 标准，因为它没有给予 stat
ic 关键字全新的含义。

最后一个特性是灵活的数组成员。有一个常见的声明结构体的问题，可能会期望这个结构体
由一个头以及接下来的一些数据类型构成。不幸的是，由于不能给结构体一个指向独立分配
区域的指针，C89 没有提供好的解决方法。两个常见的解决方案包括，声明一个成员只占一
字节存储空间，然后分配额外的超出数组边界的空间，或者声明一个成员要占用比您可能需
要的更多的空间，等待分配，而且要小心只去使用可用的存储空间。这两种方案对一些编译
器来说都会有问题，所以 C99 为此引入了一个新语法：

清单 3. 具有灵活数组的结构体

    struct header {

        size_t len;
        unsigned char data[];
    };

 

这种结构体的有用之处在于，如果您分配 (sizeof(struct header) + 10) 字节的空间，您
可以像处理一个 10 字节的数组一样来处理数据。这个新语法在 gcc 中也得到了支持。

库特性
这对编译器来说是很好的。那么标准库如何呢？基于现有的实践，尤其是来源于 BSD 和 Li
nux 社区的实践，C99 中增加了很多库特性。所以，这些特性中很多是在 Linux 和 BSD 标
准库中已经可以找到的。这些特性中很多只是简单的工具函数；几乎所有特性原则上都可以
在轻便的代码中完成，但有很多是特别难的。

C99 中增加的最方便的特性在 printf 函数家族中。首先，v*scanf 函数成为了标准；scan
f 家族的每一个成员都有一个相应的 v*scanf 函数，这些函数使用一个 va_list 参数而不
是可变的参数列表。这些函数的角色与 v*printf 函数相同，允许用户自定义获取可变参数
列表的函数，并最终调用 printf 或 scanf 家族中的函数来完成复杂的工作。

其次，引入了 4.4BSD 的 snprintf 函数家族。snprintf 函数让您可以安全地输出到固定
大小的缓冲区。当被告知输出不超过 n 个字节时，snprintf 保证会创建一个长度不超过 n
-1 的字符串，字符串最后是一个空结束符。不过，如果 n 足够大，它的返回码是将会完成
写入的字符数目。这样，您可以确切地得知您 将 需要多少缓冲区空间才可以完全格式化某
些内容。这个函数随处可用，而且您应该始终使用它；很多安全漏洞都归咎于 sprintf 中
的缓冲区溢出，而这个函数可以预防这个问题。

新标准中很多新的数学特性，包括复杂的数学特性和专用的函数，都是设计用来帮助优化特
定浮点芯片的编译器，但不能保证所有场合都已实现。如果您需要这些函数，最好先去检查
一下您的目标平台上有没有这些函数。浮点环境函数并不是总被支持，有一些平台不会支持
 IEEE 运算。现在还不要依赖于这些新特性。

C99 中对 strftime() 函数进行了扩展，以提供更多常用的格式化字符。这些新字符可能在
最新的 Linux 和 BSD 系统上都已经可用；但是在较老的系统上，它们还没有广泛可用。在
使用新格式之前先检查文档。

据说，大部分国际化代码还没有被可靠地实现。

其他新的库特性通常还没有普遍可用；数学函数在超级计算机编译器中好像已经可用了，国
际化函数在美国以外开发的编译器中可能可用。编译器厂商实现的是被要求实现的特性。

展望
一般来说，最好保守地采用新特性。不过，很多 C99 特性现在已经足够普及，因而新的开
发项目可以适当地利用它们。gcc 编译器套件已被广为应用，大部分项目完全可以假定它为
很多目标平台的一个可选项。如果您主要定位于 Linux 或 BSD 系统，或者两者兼有，您可
以依赖于大量 C99 新特性，它们至少部分地得到了支持。这些特性是根据感觉上的需要和
实际中的实现实践而采用的，您将会从中受益。

当决定您期望依赖哪些特性时，不能只是看它是否在您正在使用的机器上可用，而要考虑目
标系统（一个或多个）。您想让人们将 OS 升级到更新的发行版本吗？您的目标市场介意使
用一个新的编译器吗？在您决定使用一个特性之前，先在可能的目标系统上测试一下这个特
性。

参考资料

在 GNU 项目主页上给出了 C99 语言工作 的当前状态。新版本的 gcc 可能会改变其中一部
分。


WG14 是完成 C99 的 ISO 工作组。


Wiley 给出了 ISO C 1999 的一个硬拷贝版本，包括第一个技术勘误表 (Technical Corrig
endum ，ISBN 0-470-84573-2)。


Rationale for the C99 standard 可以以 PDF 格式在线阅读。


Kernighan 和 Ritchie 的 The C Programming Language 最早对 C 进行了描述。最初的 A
NSI 标准基于这本书的第一版，作为回报，这本书的第二版描述了 ANSI 标准。从芬兰语到
中文到犹太文再到盲语，各种版本都有。您可能还会喜欢 Dennis Ritchie's history of
C。


IBM 的 C 和 C++ 产品家族 的很多成员都符合 C99 标准，包括 VisualAge C++ for AIX V
ersion 6.0，这是一个在 AIX 和 Linux 上可用的高级 C/C++ 编译器。在 2002 年的 这份
声明（PDF 格式）中可以找到更多细节。在 C for AIX, Version 6.0, C/C++ Language Re
ference（PDF 格式）中可以找到还要多的细节。

 

从"Linux Unicode 编程"（developerWorks, 2001 年 8 月）中深入学习 Unicode 和国
际化相关的标准。


Dinkum C99 Library Reference Manual 是一个非常好的参考资料。


Are you Ready For C99? (Kuro5hin, 2001) 一文极好地对 C99 进行了概述。


Comeau Computing 公布了 Tech Talk About C99，其中带有大量举例说明新特性（从 _Boo
l 到 _func_，等等）的示例代码。


Thomas Wolf 有一个关于 C 语言 ISO 标准的信息 的网页，其中有链接指向他所做的 C99
 中大部分重要变化 的总结，还对新的函数和特性及其使用方法进行了编录。

 

David R. Tribble 的 Incompatibilities Between ISO C and ISO C++ 讨论了 C99 和 C+
+98 之间的那些抵触之处。


在 developerWorks Linux 专区 可以找到更多为 Linux 开发人员准备的参考资料。


在 Developer Bookstore 的 Linux 区 可以找到很多精选的 Linux 书籍。]]></description>
<pubDate>
2006-08-18 15:35:43.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307296.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307296.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[编写so库(Shared Object Library)]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307406.shtml</link>
<description>
<![CDATA[
在linux开发中，有很多程序需要在时间上做优化，像XFree86,mozilla等，所以需要有高精度的时间测量方法。下面给出解决方案。

1。基本函数gettimeofday
   #include <sys/time.h>
   #include <time.h>

   函数原型：int gettimeofday(struct timeval *tv, struct timezone *tz);
       struct timeval {
               time_t         tv_sec;        /* seconds */
               suseconds_t    tv_usec;  /* microseconds */
       };
       struct timezone {
               int  tz_minuteswest; /* minutes W of Greenwich */
               int  tz_dsttime;     /* type of dst correction */
       };

2。测量原理
   (1)设置基准点
   (2)在每个测试点，获取时间信息，与基准点的时间差值作为优化依据

3。示例程序
#include <sys/time.h>
#include <time.h>
void consume()
{
unsigned int i,j;
double y;
for(i=0;i<1000;i++)
for(j=0;j<1000;j++)
y=i*j;
}

main()
{
struct timeval tpstart,tpend;
float timeuse;

gettimeofday(&tpstart,NULL);
function();
gettimeofday(&tpend,NULL);
timeuse=(tpend.tv_sec-tpstart.tv_sec)+
((float)tpend.tv_usec-(float)tpstart.tv_usec)/(1000000); 
printf("Used Time:%f\n",timeuse);
}


4。编写测量库
头文件anthony_timedebug.h：
#ifndef _ANTHONY_TIMEDEBUG_H
#define _ANTHONY_TIMEDEBUG_H

#ifdef __cplusplus
extern "C"
{
#endif

#include <stdio.h>
#include <sys/time.h>
#include <time.h>

void timedebug_setstart();
void timedebug_getcurrent();


#ifdef __cplusplus
}
#endif

#endif
源文件:anthony_timedebug.c
#include "anthony_timedebug.h"

struct timeval tpstart;

void timedebug_setstart()
{
  gettimeofday(&tpstart,NULL);
}


void timedebug_getcurrent()
{
  FILE  *fp;
  struct timeval tpend;
  float  used;

  gettimeofday(&tpend,NULL);

  used = (tpend.tv_sec-tpstart.tv_sec)+
          ((float)tpend.tv_usec-(float)tpstart.tv_usec)/(1000000);

  fp = fopen("/tmp/timedebug.log","a");
  if(fp)
  {
    fprintf(fp,"%f\n",used);
    fclose(fp);
  }
}

编译目标文件：
gcc -fPIC  -c anthony_timedebug.c
编译库：
 gcc -shared -Wl,-soname,libanthony.so.1 -o libanthony.so.1.0.1 anthony_timedebug.o  -lc
安装：
cp anthony_timedebug.h /usr/include
cp libanthony.so.1.0.1 /usr/lib
cd /usr/lib
ln -s libanthony.so.1.0.1 libanthony.so
ldconfig

5.测试
测试程序test.c：
#include <anthony_timedebug.h>

void consume()
{
  int i,j;
  for(i=0;i<10000;i++)
  for(j=0;j<10000;j++);
}

main()
{
  timedebug_setstart();
  consume();
  timedebug_getcurrent();
}
编译：
gcc -o test test.c -lanthony
执行结果可以在/tmp/timedebug.log查看]]></description>
<pubDate>
2006-08-18 15:32:41.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307406.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307406.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[Linux库知识]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307484.shtml</link>
<description>
<![CDATA[linux库的知识（概）

引言：
在xmeeting中，关于usb手柄部分，采用动态库调用方式，下面翻译一篇David A. Wheeler写的文章。文章就如何创建和使用静态库，共享库以及动如何动态装载库进行了论述。内容纲要如下：
 1.概述
 2.静态库
 3.共享库
   3.1 约定
       3.2 使用
    3.3 环境变量
   3.4 创建共享库
   3.5 安装与使用
   3.6 兼容性
 4.动态加载
   4.1 dlopen()
   4.2 dlerror()
   4.3 dlsym()
   4.4 dlclose()
   4.5 示例
 5.辅助知识
   5.1 nm命令
   5.2 库的构建与析构函数
   5.3 脚本
   5.4 版本
   5.5 GNU libtool
    5.6 去除符号空间
   5.7 外部执行体
    5.8 C++ 与 C
   5.9 加速C++初始化
   5.10 Linux标准
1.概述
  本文就如何在Linux系统中运用GNU工具创建和使用程序库进行论述。所谓"程序库"，简单说，就是包含了数据和执行码的文件。其不能单独执行，可以作为其它执行程序的一部分，来完成执行功能。库的存在，可以使得程序模块化，可以加快程序的再编译，可以实现代码重用，可以使得程序便于升级。程序库可分三类：静态库，共享库和动态加载库。

  静态库，是在执行程序运行前就已经加入到执行码中，在物理上成为执行程序的一部分；共享库，是在执行程序启动时加载到执行程序中，可以被多个执行程序共享使用。动态加载库，其实并不是一种真正的库类型，应该是一种库的使用技术，应用程序可以在运行过程中随时加载和使用库。
  建议库开发人员创建共享库，比较明显的优势在于库是独立的，便于维护和更新；而静态库的更新比较麻烦，一般不做推荐。然而，它们又各有优点，后面会讲到。在C++编程中，要使用动态加载技术，需要参考文章"C++ dlopen MINI-Howto"。
  文章中讲述的执行程序和库都采用ELF(Executable and Linking Format)格式,尽管GNU GCC 工具可以处理其它格式，但不在本文的讨论范围。本文可以在http://www.dwheeler.com/program-library和http://www.linuxdoc.org找到。

2.静态库
  静态库可以认为是一些目标代码的集合。按照习惯，一般以".a"做为文件后缀名。使用ar(archiver)命令可以创建静态库。因为共享库有着更大的优势，静态库已经不被经常使用。但静态库使用简单，仍有使用的余地，并会一直存在。

  静态库在应用程序生成时，可以不必再编译，节省再编译时间。但在编译器越来越快的今天，这一点似乎已不重要。如果其他开发人员要使用你的代码，而你又不想给其源码，提供静态库是一种选择。从理论上讲，应用程序使用了静态库，要比使用动态加载库速度快1-5%,但由于莫名的原因，实际上可能并非如此。由此看来，除了使用方便外，静态库可能并非一种好的选择。

  要创建一个静态库，或要将目标代码加入到已经存在的静态库中，可以使用以下命令：
  ar rcs my_libraty.a file1.o file2.o
  以上表示要把目标码file1.o和file2.o加入到静态库my_library.a中。若my_library.a不存在，会自动创建。
 
  静态库创建成功后，需要连接到应用程序中来使用。如果你使用gcc(1)来产生执行程序，需要利用-l选项来指定静态库。更多信息，查看gcc使用手册。

  在使用gcc时，要注意其参数的顺序。-l是连接器选项，一定要放在被编译的文件名称之后；若放在文件名称之前，你会连接失败，并会出现莫名其妙的错误。这一点切记。
 
  你也可以直接使用连接器ld(1),使用其选项-l或-L。但最好使用gcc(1),因ld(1)的接口有可能会有变化。

3.共享库
  共享库是在程序启动时被装载。当一个应用程序装载了一个共享库后，其它应用程序仍可以装载同一个共享库。基于linux的使用方法，共享库还有其它灵活的而又精妙的特性：
   更新库并不影响应用程序使用旧的，非向后兼容的版本；
   在执行特定程序时，可以覆盖整个库或更新库中的特定函数；
   以上操作不会影响已经运行的程序，他们仍会使用已经装载的库。

3.1约定
   要想共享库具有以上特性，一些约定需要遵守。你需要掌握共享库名称之间的区别，特别是搜名(soname)和实名(realname)之间的区别和关系；你还需要知道共享库在文件系统的位置。
3.1.1名称
  每个共享库都有一个特定的搜名(soname),其组成如下：
  lib  +  库名  +  .so  +  .  +  version
   |       |        |_______________|
  前缀    库名            后缀
  在文件系统中，搜名是一个指向实名的符号联结。

  每个共享库也有一个实名，其真正包含有库的代码，组成如下：
  搜名 +  .   +  子版本号 + . + 发布号
  最后的句点和发布号是可选项。
 
  另外，共享库还有一个名称，一般用于编译连接，称为连名(linker name)，它可以被看作是没有任何版本号的搜名。
     

  看下面的例子：
  lrwxrwxrwx  1 root root  libpng.so -> libpng12.so
  lrwxrwxrwx  1 root root  libpng.so.2 ->   libpng.so.2.1.0.12
  -rw-r--r--  1 root root  libpng.so.2.1.0.12
  在以上信息中,  libpng.so.2.1.0.12是共享库的实名(real name)，libpng.so.2是共享库搜名(soname),libpng.so 则是连接名(linker name)，用于编译连接。

3.2共享库的装载
  在所有基于GNU glibc的系统(当然包括Linux)中，在启动一个ELF二进制执行程序时，一个特殊的程序"程序装载器"会被自动装载并运行。在linux中，这个程序装载器就是/lib/ld-linux.so.X(X是版本号)。它会查找并装载应用程序所依赖的所有共享库。
  被搜索的目录保存在/etc/ls.so.conf文件中，但一般/usr/local/lib并不在搜索之列，至少debian是这样。这似乎是一个系统失误，只好自己加上了。
  当然，如果程序的每次启动，都要去搜索一番，势必效率不堪忍受。Linux系统已经考虑这一点，对共享库采用了缓存管理。ldconfig就是实现这一功能的工具，其缺省读取/etc/ld.so.conf文件，对所有共享库按照一定规范建立符号连接，然后将信息写入/etc/ld.so.cache。 /etc/ld.so.cache的存在大大加快了程序的启动速度。

3.3创建共享库
  共享库的创建比较简单，基本有两步。首先使用-fPIC或-fpic创建目标文件，PIC或pic表示位置无关代码，然后就可以使用以下格式创建共享库了：
 gcc -share _Wl,-soname,your_soname -o library_name file_list library_list
  下面是使用a.c和b.c创建库的示例：
   gcc -fPIC -g -c -Wall a.c
   gcc -fPIC -g -c -Wall b.c
   gcc -share -Wl,-soname, libmyab.so.1 -o libmyab.so.1.0.1 a.o b.o -lc
   -g表示带有调试信息，-Wall表示产生警告信息。
  几个需要注意的地方：
  (1)不推荐使用strip处理共享库，最好不要使用-fomit-frame-pointer编译选项
  (2)-fPIC和-fpic都可以产生目标独立代码，一般采用-fPIC，尽管其产生的目标文件可能会大些；-fpic产生的代码小，执行速度快，但可能有平台依赖限制。
  (3)一般情况下，-Wall,-soname,your_soname编译选项是需要的。当然，-share选项更不能丢。

4 动态加载库
  DL技术可以允许应用程序在运行过程的任何时候去加载和使用指定的库。这一技术在插件的实现上很实用。动态加载库这一概念并不是着眼于库的文件格式，而是指使用方式。存在着一组接口函数，使得应用程序可以采用DL技术。下面对这些接口函数逐一介绍，在最后给出应用示例。

4.1 dlopen
  函数原型：void *dlopen(const char *libname,int flag);
  功能描述：dlopen必须在dlerror，dlsym和dlclose之前调用，表示要将库装载到内存，准备使用。如果要装载的库依赖于其它库，必须首先装载依赖库。如果dlopen操作失败，返回NULL值；如果库已经被装载过，则dlopen会返回同样的句柄。
  参数中的libname一般是库的全路径，这样dlopen会直接装载该文件；如果只是指定了库名称，在dlopen会按照下面的机制去搜寻：
  (1)根据环境变量LD_LIBRARY_PATH查找
  (2)根据/etc/ld.so.cache查找
  (3)查找依次在/lib和/usr/lib目录查找。
  flag参数表示处理未定义函数的方式，可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暂时不去处理未定义函数，先把库装载到内存，等用到没定义的函数再说；RTLD_NOW表示马上检查是否存在未定义的函数，若存在，则dlopen以失败告终。

4.2 dlerror
  函数原型：char *dlerror(void);
  功能描述：dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息，返回NULL表示无错误。dlerror在返回错误信息的同时，也会清除错误信息。

4.3 dlsym
  函数原型：void *dlsym(void *handle,const char *symbol);
  功能描述：在dlopen之后，库被装载到内存。dlsym可以获得指定函数(symbol)在内存中的位置(指针)。如果找不到指定函数，则dlsym会返回NULL值。但判断函数是否存在最好的方法是使用dlerror函数，下面是示例：
  dlerror();/*清除错误信息*/
  function = dlsym(handle,"function_name");
  if((error=dlerror()) != NULL)
  {
    /*错误处理*/
  }
  else
  {
    /*找到函数*/
  }

4.4 dlclose
  函数原型：int dlclose(void *);
  功能描述：将已经装载的库句柄减一，如果句柄减至零，则该库会被卸载。如果存在析构函数，则在dlclose之后，析构函数会被调用。

4.5动态加载库示例
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

int main(int argc,char **argv)
{
  void *handle;
  double (*cosine)(double);
  char *error;

  handle = dlopen("/lib/libm.so.6",RTLD_LAZY);
  if(!handle)
  {
    printf("%s\n",dlerror());
    exit(1);
  }

  printf("opened /lib/libm.so.6\n");

  cosine = dlsym(handle,"cos");
  if((error = dlerror()) != NULL)
  {
    printf("%s\n",error);
    dlclose(handle);
    printf("after error,closed /lib/libm.so.6\n");
    exit(1);
  }

  printf("%f\n",(*cosine)(2.0));

  dlclose(handle);
  printf("closed /lib/libm.so.6\n");

  return 0;
}
  编译：gcc -o test test.c -ldl。在这个例子中，/lib/libm.so.6是动态加载库，而/usr/lib/libdl.so则是共享库。


5.相关知识
5.1 nm命令
  nm(1)命令可以报告库的符号列表，对于查看库的相关信息是一个不错的工具。具体使用查看帮助文档。示例：
＄nm -D libavcodec-0.4.7.so | grep 263
结果如下：
00116438 T ff_clean_h263_qscales
00122d14 T ff_h263_decode_end
001223d4 T ff_h263_decode_frame
00121340 T ff_h263_decode_init
0011048c T ff_h263_decode_mb
0011652c T ff_h263_get_gob_height
0010e664 T ff_h263_resync
00041744 T ff_h263_round_chroma
0010810c T ff_h263_update_motion_val
00115f64 T flv_h263_decode_picture_header
0010da98 T h263_decode_init_vlc
001127c8 T h263_decode_picture_header
001ab040 D h263_decoder
00106c44 T h263_encode_gob_header
0010b2b0 T h263_encode_init
00109d40 T h263_encode_mb
00105f94 T h263_encode_picture_header
001a85a0 D h263_encoder
001162d0 T h263_get_picture_format
0010a7b4 T h263_pred_motion
00106df8 T h263_send_video_packet
001ab180 D h263i_decoder
001a85e0 D h263p_encoder
00115c68 T intel_h263_decode_picture_header
其中，T表示正常代码段，D表示初始化数据段

5.2库的构建与析构函数
  关于构建与析构函数，一般不需要自己去编程实现。如果你一定要自己做，下面是函数原型：
  void __attribute__ ((constructor)) my_init(void);
  void __attribute__ ((destructor)) my_fini(void);
  在编译共享库时，不能使用"-nonstartfiles"或"-nostdlib"选项，否则，构建与析构函数将不能正常执行(除非你采取一定措施)。

5.3脚本共享库
  linux中,共享库可以是脚本形式，当然需要专门的脚本语言。/usr/lib/libc.so是一个典型的例子，内容如下：
  /* GNU ld script
     Use the shared library, but some functions are only in
     the static library, so try that secondarily.  */
  OUTPUT_FORMAT(elf32-i386)
  GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a )

5.4 版本脚本(略)
5.5 GNU libtool(略)

5.6除去记号信息
  共享库中的记号信息多为调试之用，但占用了磁盘空间。如果你的库是为嵌入式系统所用，最好去掉记号信息。一种方法，利用strip(1)命令，使用方法查看其帮助文档；另一种方法，使用GNU LD选项-s或-S,例如"-Wl -s"或"-Wl -S"。-S仅除去调试记号信息；-s除去所有记号信息。

5.7编译优化
  有一篇文章写的不错"Whirlwind tutorial On Creating really teensy ELF Executables For Linux"。这篇文章中可以说把程序的代码优化到了极点。在我们实际的应用中，可能并需要那些技巧，但通过此文，我们可以更多的了解ELF。

5.8 C++与C
  要使得你编写的共享库能同时被C和C++程序使用，库的头文件需要使用"extern C"预定义，下面是一个例子：
  #ifndef LIB_HELLO_H
  #define LIB_HELLO_H

  #ifdef __cplusplus
  extern "C"
  {
  #endif


  .....头文件代码
 
  #ifdef __cplusplus
  }
  #endif

  #endif

5.9关于C++程序的启动速度
  C++应用程序的启动速度是比较慢的。我一直使用firefox,感受颇深。有人认为这是因主函数启动之前的代码重定位所导致。有一篇文章"making C++ ready for the desktop"(by Waldo Bastian)对这问题作了分析。我读了一下，理解不是很深刻。

5.10 Linux Standard Base(LSB)
  LSB是一个项目，致力于制订和推动一系列标准，尽力提高不同Linux发布版本之间的兼容性，从而为应用程序的开发提供一致性的接口。关于linux标准项目的详细信息，可查阅网站www.linuxbase.org。]]></description>
<pubDate>
2006-08-18 15:29:50.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307484.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307484.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[uClinux移植与分析]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307568.shtml</link>
<description>
<![CDATA[uClinux移植与分析
简介：
  前一段时间，曾先后移植了uClinux-2.0.x和uClinux-2.4.x的内核，
我的移植基本上是从零做起，linux并没有支持该目标机的代码，所以这
个移植工作基本上是新增加对一种目标机的支持。

  工作过程中，我学到了不少知识，除了操作系统，还了解了一些编
译，调试，汇编，链接的的技术，在此我会一并介绍，可能介绍比较多
的是连接器，因为这个相对和操作系统联系更加紧密一些。
  我希望能够与大家分享自己经验，同时，有错误和不当的地方欢迎
网友指出，共同进步，这是我写这些原创帖的动力。

  “编程并非零和的游戏。将己所知教给程序员同胞，他们并不会夺你
所知。能将我所知与人分享，我感到高兴，因为我身在其中、热爱编程。”
                                ——John Carmack

uClinux下用户程序的执行

  之所以从用户程序谈起，是因为我们平常接触最多的还是应用程序。
从应用程序引出到操作系统我觉得比较自然。下面就从一个简单例子介
绍一个程序如何在操作系统中运行。

  假如有个c程序：
  int main(int argc, char **argv[])
  {
    printf("hello world!\n");
    return 0;
  }

  这是一个最简单不过的程序了，一般一个C语言程序，都从main开始
执行。那么，main函数是不是与其他函数有所区别，地位有些特殊呢？
不是的。main函数和其他函数地位一样。其实，我们完全可以做到让一个
c程序从任何地方开始执行。比如linux，它就没有main函数，大家都知道，
系统执行过启动的一段汇编后，就会跳转到位于init/main.c中的
start_kernel中开始执行。

  那么为什么用户程序都要从main函数执行呢？这就是用户C库的原因。
一般用户用c语言开发时会调用一些库函数，编译成obj文件后，在链接过
程中把库函数的二进制代码链接进入程序，最后形成二进制可执行文件。
链接过程中，链接器会在用户程序前插入一些初始化的代码。uClinux下
是在crt0.s中(我移植的是uClibc库)。不管什么平台下什么形式的crt0.s，
这个文件最后几行代码中肯定有一个jmp(或者call或br等转移指令) main
(或__uClibc_main)。这就是为什么你的程序都从main开始执行。如果你把
这个跳转标号改成任意一个标号，比如foo。而你的程序里面既有main，又
有foo，则这种情况下，程序就先从foo开始执行。所以，main函数和其他
函数一样，并没有特殊地位。

  下面谈谈在uClinux中，main函数的argc,argv是参数怎样传递的。我们
以flat格式可执行文件为例。uClinux下支持一种叫flat的可执行文件格式。
这种文件格式比较简单，基本上是平铺的，所以叫flat很形象。现在好像
uClinux-2.4.x内核的版本已经能够支持elf格式的文件执行了。不过为了
举例简单，我还是用flat格式举例。这里暂不分析flat文件格式，我们把注
意力放到参数传递上。uClinux开发用户程序，首先当然是编码，然后编译，
编译生成的文件是elf格式的，所以要用工具elf2flt将elf文件转换成flat，
假设这个工作已经完成。

  我们在uclinux的shell下执行一个文件foo x y，foo是程序名，x, y是
参数。学过C语言的都知道，x，y作为参数会传递给main，其中argc＝3，
argv[0]="foo", argv[1]="x", argv[2]="y"。这些参数是如何传递进来的呢。
在你执行一个程序的时候，操作系统会调用
程中把库函数的二进制代码链接进入程序，最后形成二进制可执行文件。
链接过程中，链接器会在用户程序前插入一些初始化的代码。uClinux下
是在crt0.s中(我移植的是uClibc库)。不管什么平台下什么形式的crt0.s，
这个文件最后几行代码中肯定有一个jmp(或者call或br等转移指令) main
(或__uClibc_main)。这就是为什么你的程序都从main开始执行。如果你把
这个跳转标号改成任意一个标号，比如foo。而你的程序里面既有main，又
有foo，则这种情况下，程序就先从foo开始执行。所以，main函数和其他
函数一样，并没有特殊地位。

  下面谈谈在uClinux中，main函数的argc,argv是参数怎样传递的。我们
以flat格式可执行文件为例。uClinux下支持一种叫flat的可执行文件格式。
这种文件格式比较简单，基本上是平铺的，所以叫flat很形象。现在好像
uClinux-2.4.x内核的版本已经能够支持elf格式的文件执行了。不过为了
举例简单，我还是用flat格式举例。这里暂不分析flat文件格式，我们把注
意力放到参数传递上。uClinux开发用户程序，首先当然是编码，然后编译，
编译生成的文件是elf格式的，所以要用工具elf2flt将elf文件转换成flat，
假设这个工作已经完成。

  我们在uclinux的shell下执行一个文件foo x y，foo是程序名，x, y是
参数。学过C语言的都知道，x，y作为参数会传递给main，其中argc＝3，
argv[0]="foo", argv[1]="x", argv[2]="y"。这些参数是如何传递进来的呢。
在你执行一个程序的时候，操作系统会调用
do_execve（char *filename, char**argv, char**envp, struct pt_regs *regs)，
这个操作会根据文件路径打开文件，装入内存，argv就是放到命令行参数，envp是
环境变量参数。

  在装入文件时，系统会根据不同的文件格式调用不同文件装入的handler，如果
是flat格式，就会调用load_flat_binary()，在fs/binfmt_flat.c中。有关参数，会
根据一路传递下来的argv,envp首先处理一遍计算出参数的个数argc,envc。然后在函
数create_flat_tables里面建立好参数表。整个函数代码如下：
static unsigned long create_flat_tables(unsigned long pp, struct linux_binprm *
bprm)
{
(1) unsigned long *argv,*envp;
(2) unsigned long * sp;
(3) char * p = (char*)pp;
(4) int argc = bprm->argc;
(5) int envc = bprm->envc;
(6) char dummy;

(7) sp = (unsigned long *) \
        ((-(unsigned long)sizeof(char *))&(unsigned long) p);

(8) sp -= envc+1;
(9) envp = sp;
(10)   sp -= argc+1;
(11)   argv = sp;

(12)   flat_stack_align(sp);
(13)   if (flat_argvp_envp_on_stack()) {
(14)     --sp; put_user((unsigned long) envp, sp);
(15)     --sp; put_user((unsigned long) argv, sp);
(16)   }

(17)   put_user(argc,--sp);
(18)   current->mm->arg_start = (unsigned long) p;
(19)   while (argc-->0) {
(20)     put_user((unsigned long) p, argv++);
(21)     do {
(22)         get_user(dummy, p); p++;
(23)     } while (dummy);
(24)   }
(25)   put_user((unsigned long) NULL, argv);
(26)   current->mm->arg_end = current->mm->env_start = (unsigned long) p;
(27)   while (envc-->0) {
(28)     put_user((unsigned long)p, envp); envp++;
(29)     do {
(30)         get_user(dummy, p); p++;
(31)     } while (dummy);
(32)   }
(33)   put_user((unsigned long) NULL, envp);
(34)   current->mm->env_end = (unsigned long) p;
(35)   return (unsigned long)sp;
}
  (1)-(6)行是变量声明。其中argc和envc分别记录前面已经计算出来的参数个数和
环境变量参数个数。p=pp是参数和环境变量数组的指针，sp是你要执行程序的用户区
堆栈，就是foo程序执行时，用户空间堆栈的起始地址。(8)-(11)是一个堆栈调整。首
先sp移动envc+1个单位，这envc+1个用来存放一共envc个envp[0]->envc[envp-1]元素
地址的，多余一个放0，表示envp数组结束。然后sp在移动argc+1各单位，留出argc+1
单位空间，这argc+1个单位是用来存放argc个argv[0]->argv[argc-1]元素地址的，多
余一个也放0，表示argv数组结束。经过堆栈调整，argv和envp各自指向自己在堆栈中
的位置。如果开始堆栈初值记为init_sp，则现在envp=init_sp-(envc+1)，
argv=envp-(argc+1)。

  (12)无关紧要，略去不提。(13)-(17)又是一次堆栈调整。(14)是sp再移动1个单
位,然后将envp放入这个地址(此时envp=init_sp-(envc+1)),然后(15)又将sp移动一个
单位,将argv写入. (17)是移动堆栈后将argc也写入里面.

  (18)-(35)行是将argv[0]->argv[argc-1](在p所指向地方)依次写入argv所指堆栈
区域中.然后再将envp[0]->edummy, p); p++;
(31)     } while (dummy);
(32)   }
(33)   put_user((unsigned long) NULL, envp);
(34)   current->mm->env_end = (unsigned long) p;
(35)   return (unsigned long)sp;
}
  (1)-(6)行是变量声明。其中argc和envc分别记录前面已经计算出来的参数个数和
环境变量参数个数。p=pp是参数和环境变量数组的指针，sp是你要执行程序的用户区
堆栈，就是foo程序执行时，用户空间堆栈的起始地址。(8)-(11)是一个堆栈调整。首
先sp移动envc+1个单位，这envc+1个用来存放一共envc个envp[0]->envc[envp-1]元素
地址的，多余一个放0，表示envp数组结束。然后sp在移动argc+1各单位，留出argc+1
单位空间，这argc+1个单位是用来存放argc个argv[0]->argv[argc-1]元素地址的，多
余一个也放0，表示argv数组结束。经过堆栈调整，argv和envp各自指向自己在堆栈中
的位置。如果开始堆栈初值记为init_sp，则现在envp=init_sp-(envc+1)，
argv=envp-(argc+1)。

  (12)无关紧要，略去不提。(13)-(17)又是一次堆栈调整。(14)是sp再移动1个单
位,然后将envp放入这个地址(此时envp=init_sp-(envc+1)),然后(15)又将sp移动一个
单位,将argv写入. (17)是移动堆栈后将argc也写入里面.

  (18)-(35)行是将argv[0]->argv[argc-1](在p所指向地方)依次写入argv所指堆栈
区域中.然后再将envp[0]->envp[envc-1](也是由p所指)写入envp所指的堆栈区域中.
在写入同时,还要设置进程控制块相应的数据结构,如arg_start,env_start,env_end等.

  下面举例和画图来说明过程.比如执行foo x y,此时argc=3,argv[0]="foo",
argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假设用户堆栈起始
空间堆栈地址是sp=0x1f0000,pp=0x1c0000.则处理过后在foo执行前,他的用户空
间堆栈frame如下:


      --------------------------------
0x1f0000   |       0000           |
      --------------------------------
0x1efffc   | envp[0] = 0x1c0008       | ---->指向"path=/bin"
      --------------------------------
0x1efff8   |       0000           |
      --------------------------------
0x1efff4   | argv[2] = 0x1c0006       | ----->指向"y"
      --------------------------------
0x1efff0   | argv[1] = 0x1c0004       | ----->指向"x"
      --------------------------------
0x1effec   | argv[0] = 0x1c0000       | ----->指向"foo"
      --------------------------------
0x1effe8   | start addr of envp = 0x1efffc|
在写入同时,还要设置进程控制块相应的数据结构,如arg_start,env_start,env_end等.

  下面举例和画图来说明过程.比如执行foo x y,此时argc=3,argv[0]="foo",
argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假设用户堆栈起始
空间堆栈地址是sp=0x1f0000,pp=0x1c0000.则处理过后在foo执行前,他的用户空
间堆栈frame如下:


      --------------------------------
0x1f0000   |       0000           |
      --------------------------------
0x1efffc   | envp[0] = 0x1c0008       | ---->指向"path=/bin"
      --------------------------------
0x1efff8   |       0000           |
      --------------------------------
0x1efff4   | argv[2] = 0x1c0006       | ----->指向"y"
      --------------------------------
0x1efff0   | argv[1] = 0x1c0004       | ----->指向"x"
      --------------------------------
0x1effec   | argv[0] = 0x1c0000       | ----->指向"foo"
      --------------------------------
0x1effe8   | start addr of envp = 0x1efffc|
到r2-r6里来传递。当然，如果超过5个，就要借助堆栈了。

  既然main带了参数，那么在调用main之前，要把argc放到r2里面，argv放到r3里
面，envp放到r4里面。刚才说了，sp是用户空间堆栈起始地址。所以在开始执行foo
代码时候，r0=sp，在上文例子里r0等于0x1effe0.则如下伪汇编代码可以让参数装入
正确寄存器。

  load   r2, (r0)     /* r2 = argc */
  load   r3, (r0, 4)   /* r3 = argv */
  load   r4, (r0, 8)   /* r4 = envp */
  call   main       /* 跳转到main函数 */

  call   _exit

  以上代码就是最简单的进入main函数前的预处理。当然，不同系统不同格式文件处
理方式是不同的，刚才的一些例子是我碰到的一些情景和解决方案。

  我这个程序例子还没有完全讲完，比如后面printf怎么处理等，不过手都酸了，就
先讲到main函数的参数传递吧。刚学c语言那阵觉得main挺神秘，做过系统就知道，其实
main跟别的函数没有任何区别:)

printf和标准输出

  上次写到main函数的参数传递.现在继续往下进行.最近忙实验室的事情,看了一周
的文章,也没啥进展,周末写点技术贴,放松一下:-)

  进入main函数后,就要调用printf("Hello World!\n");了.顺便将C语言参数传递提
一下.字符串"Hello World!\n"编译器是当作字符串常量来处理的,虽然printf是在main
内部调用,但"Hello World!\n"可不是放在main的栈中,字符串常量至少是放到.data段的
,准确说是放在只读数据段.rodata,这个我在工作站上验证了一把.假如编辑的文件名是
hello.c,首先编译生成elf格式二进制文件gcc hello.c -o hello然后用命令
objdump -s hello -s参数会将所有段信息dump出来.你会看到"Hello World!\n"位于
.rodata段.

  printf()是个标准C库函数.虽然功能简单,但实现起来却不容易.这是个和平台相关的
函数.在pc上,printf输出是输出到终端屏幕,在嵌入式设备上,一般printf()是输出到串口.
同是调用printf(),最终输出的设备却不同,从直觉的肯定是感觉printf()底层和平台是相
关的.那么printf()是怎样实现的呢?

  可以看一下C库程序的代码,这里以uClibc为例.
  int printf(const char * __restrict format, ...)
  {
  va_list arg;
  int rv;

  va_start(arg, format);
  rv = vfprintf(stdout, format, arg);
  va_end(arg);

  return rv;
  }
  printf支持字符串格式化输出,具体参数处理这里不提.可以看到printf()调用了
vfprintf(),vfprintf()第一个参数是stdout是标准输出设备.标准输出设备是个结构体,
最重要的成员就是他的描述符,其值为1.

  跟进vfprintf()函数看,里面是复杂的参数处理,因为printf()的参数形式很灵活,
所以在vfprintf()里面要对传进来的参数进行解析处理,形成最终的输出格式.有兴趣的
可以看一下,借助这个可以让你在一个没有操作系统的平台上实现你自己的printf()
函数.这样你在裸机上调程序时输出调试信息就更方便些(实际上uClinux的printk就是
这么干的).

  vfprintf()在参数处理之后，就是输出了，输出调用的是putc()，进入putc()然后
再跟进几层函数，发现调用了linux系统调用write()。呵呵，是的，输出就是借助操作
系统代码完成的。在write之前所有的代码都是C库的代码，可以说是和平台无关的。
而涉及到具体输出，就要调用操作系统提供给你的接口。系统调用的原理就是通过一定
手段（一般是trap陷阱）进入操作系统的内核空间，调用操作系统代码来完成某些任务。

 Linux系统调用针对不同平台有不同的实现方式。这个以后再讲。调用write()后，
进入内核空间，首先来到的就是sys_write()，这个函数代码位于fs/read_write.c中。
一进入sys_write()，就要根据传进来的fd描述符找到相应的file结构。对于标准输出，
fd=1，每个进程的进程控制块都有一个打开文件的数组。file结构就是根据fd在这个
数组中查找到相应的结构。找到结构后，就会调用file->write()来向外输出。具体输出
到哪里，就要看file结构对应的设备驱动是什么。一般嵌入式系统可以从串口将信息输
出，那么file->write()最底层就是调用的串口驱动的类似transmit_char的函数。

  有关linux的设备驱动有很多书介绍,整个驱动的结构很复杂,我这里也没必要提了.
至于终端设备怎样挂在驱动队列里面,怎么根据标准输出的描述符找到相应的驱动结构
有兴趣的莊printf()函数看,里面是复杂的参数处理,因为printf()的参数形式很灵活,
所以在vfprintf()里面要对传进来的参数进行解析处理,形成最终的输出格式.有兴趣的
可以看一下,借助这个可以让你在一个没有操作系统的平台上实现你自己的printf()
函数.这样你在裸机上调程序时输出调试信息就更方便些(实际上uClinux的printk就是
这么干的).

  vfprintf()在参数处理之后，就是输出了，输出调用的是putc()，进入putc()然后
再跟进几层函数，发现调用了linux系统调用write()。呵呵，是的，输出就是借助操作
系统代码完成的。在write之前所有的代码都是C库的代码，可以说是和平台无关的。
而涉及到具体输出，就要调用操作系统提供给你的接口。系统调用的原理就是通过一定
手段（一般是trap陷阱）进入操作系统的内核空间，调用操作系统代码来完成某些任务。

  Linux系统调用针对不同平台有不同的实现方式。这个以后再讲。调用write()后，
进入内核空间，首先来到的就是sys_write()，这个函数代码位于fs/read_write.c中。
一进入sys_write()，就要根据传进来的fd描述符找到相应的file结构。对于标准输出，
fd=1，每个进程的进程控制块都有一个打开文件的数组。file结构就是根据fd在这个
数组中查找到相应的结构。找到结构后，就会调用file->write()来向外输出。具体输出
到哪里，就要看file结构对应的设备驱动是什么。一般嵌入式系统可以从串口将信息输
出，那么file->write()最底层就是调用的串口驱动的类似transmit_char的函数。

  有关linux的设备驱动有很多书介绍,整个驱动的结构很复杂,我这里也没必要提了.
至于终端设备怎样挂在驱动队列里面,怎么根据标准输出的描述符找到相应的驱动结构
有兴趣的请查阅相关资料.
--


手段（一般是trap陷阱）进入操作系统的内核空间，调用操作系统代码来完成某些任务。

  Linux系统调用针对不同平台有不同的实现方式。这个以后再讲。调用write()后，
进入内核空间，首先来到的就是sys_write()，这个函数代码位于fs/read_write.c中。
一进入sys_write()，就要根据传进来的fd描述符找到相应的file结构。对于标准输出，
fd=1，每个进程的进程控制块都有一个打开文件的数组。file结构就是根据fd在这个
数组中查找到相应的结构。找到结构后，就会调用file->write()来向外输出。具体输出
到哪里，就要看file结构对应的设备驱动是什么。一般嵌入式系统可以从串口将信息输
出，那么file->write()最底层就是调用的串口驱动的类似transmit_char的函数。

  有关linux的设备驱动有很多书介绍,整个驱动的结构很复杂,我这里也没必要提了.
至于终端设备怎样挂在驱动队列里面,怎么根据标准输出的描述符找到相应的驱动结构
有兴趣的请查阅相关资料.
进程切换部分代码实现

  移植linux,修改的主要就是和平台相关的那部分代码.linux里面和平台相关的代码
包括很多方面,比如boot过程,系统调用,中断处理,设备驱动,还有部分信号(软中断)处理
等,进程切换也有很小一部分平台相关代码.相对其它部分,我觉得这部分平台相关代码还
是相对简单的.

  schedule()是uClinux中实现进程调度的函数.通过一定算法,进行调度.假设有2各进
程a,b,a运行时,调用了schedule(),那么os就要从进程就绪队列中挑选一个合适的进程,
如果没有合适进程,则后面继续运行a,假设找到了合适进程b,则就要从当前进程a切换到b.
这个切换过程是在switch_to()中进行的.

  switch_to()出现在schedule()函数里面。调用形式switch_to(prev, next, last);
prev,next都是进程控制块task_struct的指针.prev指向当前运行的进程,next指向要切
换的进程.

  讲一下我移植的代码.由于代码是汇编程序，首先介绍一下cpu结构。我用的cpu采用
16位指令，32位的地址和数据。有16个通用寄存器，记作r0-r15。r0作为堆栈指针寄存器
sp,r1用途不固定，r2-r6作为参数传递寄存器，函数调用如果有不超过5个的参数，则参
数从左至右依次放在r2-r6中。同时，r2还作为函数返回值寄存器，函数的返回值都放在
r2里面。r7-r14是局部变量寄存器。r15是函数返回地址寄存器，也叫link register，
存放的是function call地返回地址。

  #define switch_to(prev,next,last) {           \
(1) register void *_prev __asm__ ("r2") = (prev);   \
(2) register void *_next __asm__ ("r3") = (next);   \
(3) register void *_last;                   \
(4)       __asm__ __volatile__(             \
(5)       "jbsr " SYMBOL_NAME_STR(resume) "\n\t" \
(6)       "mfcr %0, ss4"                 \
(7)         : "=r" (_last)               \
(8)         : "r" (_prev),               \
(9)           "r" (_next)                 \
(10)         : "r2", "r2", "r3");           \
(11) (last) = _last;                       \
  }
换的进程.

  讲一下我移植的代码.由于代码是汇编程序，首先介绍一下cpu结构。我用的cpu采用
16位指令，32位的地址和数据。有16个通用寄存器，记作r0-r15。r0作为堆栈指针寄存器
sp,r1用途不固定，r2-r6作为参数传递寄存器，函数调用如果有不超过5个的参数，则参
数从左至右依次放在r2-r6中。同时，r2还作为函数返回值寄存器，函数的返回值都放在
r2里面。r7-r14是局部变量寄存器。r15是函数返回地址寄存器，也叫link register，
存放的是function call地返回地址。

  #define switch_to(prev,next,last) {           \
(1) register void *_prev __asm__ ("r2") = (prev);   \
(2) register void *_next __asm__ ("r3") = (next);   \
(3) register void *_last;                   \
(4)       __asm__ __volatile__(             \
(5)       "jbsr " SYMBOL_NAME_STR(resume) "\n\t" \
(6)       "mfcr %0, ss4"                 \
(7)         : "=r" (_last)               \
(8)         : "r" (_prev),               \
(9)           "r" (_next)                 \
(10)         : "r2", "r2", "r3");           \
(11) (last) = _last;                       \
  }

  switch_to()所做的工作其实相当于为调用resume做一些准备。(1)-(2)的意思是将变
量_prev,_next分别放在寄存器r2,r3里面，他们的值分别等于prev和next，就是两个
task_struct的指针。这么做是为调用resume准备好参数。第三行是声明一个寄存器临时变
量_last。

  第(5)行是调用resume函数实现进程切换。jbsr是一条跳转指令，字面意思是跳入到子
程序(jump to subroutine)，这条指令做的工作是将现将当前pc+2保存到r15中（因为是16
位指令，所以+2），相当于保存函数的返回值，然后再将pc设置成汇编指令参数中给出的
地址（就是跳转，这里就是resume的地址）。

  第(6)行是将控制寄存器ss4内容放到_last对应的寄存器中。这一行指令有一些
trick,先讲指令所做的操作，再讲为什么这样做。mfcr是从控制寄存器移动到通用寄
存器的指令。cpu除了有16个通用寄存器，还有16各控制寄存器。所有涉及控制寄存器
的操作都要在cpu的超级用户模式下进行。cpu模式切换通过设置第0号控制寄存器来完
成。16个控制寄存器分别为cr0-cr15，其中cr0也叫psr是程序状态寄存器。cr6-cr10
也叫ss0-ss4是用于保存状态的寄存器。第（6）代码就是将ss4内容放入到变量_last
所对应的寄存器中。

  (7)-(10)行的意义请参考AT&T汇编。

  (11)行是一个赋值，last=_last。

  switch_to()所做的工作其实相当于为调用resume做一些准备。(1)-(2)的意思是将变
量_prev,_next分别放在寄存器r2,r3里面，他们的值分别等于prev和next，就是两个
task_struct的指针。这么做是为调用resume准备好参数。第三行是声明一个寄存器临时变
量_last。

  第(5)行是调用resume函数实现进程切换。jbsr是一条跳转指令，字面意思是跳入到子
程序(jump to subroutine)，这条指令做的工作是将现将当前pc+2保存到r15中（因为是16
位指令，所以+2），相当于保存函数的返回值，然后再将pc设置成汇编指令参数中给出的
地址（就是跳转，这里就是resume的地址）。

  第(6)行是将控制寄存器ss4内容放到_last对应的寄存器中。这一行指令有一些
trick,先讲指令所做的操作，再讲为什么这样做。mfcr是从控制寄存器移动到通用寄
存器的指令。cpu除了有16个通用寄存器，还有16各控制寄存器。所有涉及控制寄存器
的操作都要在cpu的超级用户模式下进行。cpu模式切换通过设置第0号控制寄存器来完
成。16个控制寄存器分别为cr0-cr15，其中cr0也叫psr是程序状态寄存器。cr6-cr10
也叫ss0-ss4是用于保存状态的寄存器。第（6）代码就是将ss4内容放入到变量_last
所对应的寄存器中。

  (7)-(10)行的意义请参考AT&T汇编。

  (11)行是一个赋值，last=_last。

  其实，上面并不是一个非常优化的做法。完全可以省掉_last变量，不过当初我做时
，看到m68k版本用了_last变量，而又不很清楚他的作用，为防止出错，照办了过来。其
实经过后面分析，可知这个变量其实是冗余的。

  那么，为什么要有(6)和(11)行的代码呢？回头可以看一下schedule()的代码，在
switch_to()调用过后，schedule()中调用了schedule_tail(prev)函数。显然prev作为
参数，应该放到r2里面，所以就有了switch_to()代码的第(11)行。那么为什么prev是来
自ss4呢？

  在调用resume之前，prev存放在r2中。r2中的内容属于进程的上下文，在做进程切
换时，要存放在栈中。同时切换到另一个进程时，还要将另一个进程的上下文装入到寄
存器中。在装入新进程时，r2的值就会被冲掉。举个例子，比如你通过fork系统调用创
建了一个新进程。我们知道，fork地返回值如果是0就表示子进程，大于0就是父进程。
对于子进程，这个栈里r2就是0（前面说过，r2用作放函数返回值），如果此时schedule
选了一个经fork后的子进程开始执行，则切换到该子进程后，其r2显然为0，当然就不
是prev了。所以，我的实现是在进程切换时，将r2值存放在ss4中，切换完毕后，再进
行区别对待。如果是两个已经运行过的进程切换，则返回就返回到原来switch_to的地
方。如果是新的fork出来的进程，则第一次调用，在resume返回时，返回的是
ret_from_fork，这是另外处理的。

  (11)行是一个赋值，last=_last。

  其实，上面并不是一个非常优化的做法。完全可以省掉_last变量，不过当初我做时
，看到m68k版本用了_last变量，而又不很清楚他的作用，为防止出错，照办了过来。其
实经过后面分析，可知这个变量其实是冗余的。

  那么，为什么要有(6)和(11)行的代码呢？回头可以看一下schedule()的代码，在
switch_to()调用过后，schedule()中调用了schedule_tail(prev)函数。显然prev作为
参数，应该放到r2里面，所以就有了switch_to()代码的第(11)行。那么为什么prev是来
自ss4呢？

  在调用resume之前，prev存放在r2中。r2中的内容属于进程的上下文，在做进程切
换时，要存放在栈中。同时切换到另一个进程时，还要将另一个进程的上下文装入到寄
存器中。在装入新进程时，r2的值就会被冲掉。举个例子，比如你通过fork系统调用创
建了一个新进程。我们知道，fork地返回值如果是0就表示子进程，大于0就是父进程。
对于子进程，这个栈里r2就是0（前面说过，r2用作放函数返回值），如果此时schedule
选了一个经fork后的子进程开始执行，则切换到该子进程后，其r2显然为0，当然就不
是prev了。所以，我的实现是在进程切换时，将r2值存放在ss4中，切换完毕后，再进
行区别对待。如果是两个已经运行过的进程切换，则返回就返回到原来switch_to的地
方。如果是新的fork出来的进程，则第一次调用，在resume返回时，返回的是
ret_from_fork，这是另外处理的。

  上面说了这么多，可能读者还是糊里糊涂的，我也觉得自己没说清楚，所以这里的
这点实现有那么一点点trick，需要对cpu的ABI和linux的内核代码非常熟悉才行。
    (11)ldw   r7, (r0)         /* restore r7 */
    (12)ldw   r8, (r0, 4)       /* restore r8 */
    (13)addi   r0, 8
    (14)SAVE_SWITCH_STACK
    (15)lrw   r8, TASK_THREAD   /* the position of thread in task_struct */
    (16)addu   r8, r2
    (17)mfcr   r6, ss1           /* Get current usp */
    (18)stw   r6, (r8, THREAD_USP) /* Save usp in task struct */
    (19)stw   r0, (r8, THREAD_KSP) /* Save ksp in task struct */

    (20)lrw   r8, TASK_THREAD
    (21)lrw   r7, SYMBOL_NAME(_current_task)
    (22)stw   r3, (r7)         /* Set new task */
    (23)addu   r8, r3           /* Pointer to thread in task_struct */

    /* Set up next process to run */
    (24)ldw   r0, (r8, THREAD_KSP) /* Set next ksp */
    (25)ldw   r6, (r8, THREAD_USP) /* Set next usp */
    (26)mtcr   r6, ss1
    (27)ldw   r7, (r8, THREAD_SR)   /* Set next PSR */
    (28)mtcr   r7, psr
    (29)RESTORE_SWITCH_STACK
              ----------------
              |   r11     |
              ----------------
              |   r10     |
              ----------------
              |   r9     |
              ----------------
              |   r8     |
              ----------------
              |   r7     |
              ----------------
              |   r6     |
              ----------------
              |   r5     |
              ----------------
              |   r4     |
              ----------------
              |   r3     |
              ----------------
              |   r2     |
              ----------------
        0x1effC4 |   r1     |
              ----------------
0x1f0000和0x1effc4分别是执行过(14)前后r0的值。这是个contex save的操作。

注：lrw是立即数装入操作，addu是无符号加法，mfcr和mtcr是控制寄存器移动
  操作，bclri是位清除操作，ldw是load word操作，addi是立即数加法操作。

  (15)-(19)是做的栈指针保存操作。将当前进程用户栈和内核栈保存到进程
控制块相应的数据结构中。linux下除了内核线程（只有内核栈）每个进程都有
2个栈，一个在用户空间一个在内核空间。如果是内核线程，则不用关心它的用
户堆栈，反正不会用到，是什么值都可以。如果用户进程，则在用户进程执行
系统调用或者在用户进程执行时发生中断时，都需要从用户空间进入内核空间，
在进入时，原先的用户空间栈指针就会暂时存放到ss1中。所以(17)-(18)两行
就是从ss1中取出用户空间栈指针，存入task_struct中。(15)-(19)的操作可
以总结为：
  prev->thread.usp = ss1 保存用户栈指针
  prev->thread.ksp = r0   保存内核栈指针

  那么有人可能会问，ss1能够保证就是正确的当前用户栈指针么？当然可以。
因为内核线程没有用户栈，所以这个值是什么无所谓。而对于用户进程，进入
resume的唯一入口就是schedule，而这又都是操作系统内核代码。用户进程进
入内核手段就有系统调用和中断，而在系统调用和中断处理一进来就保存了用
户堆栈到ss1，所以在运行时，只要在内核里用的都是内核栈，用户栈指针不会
变。

  (20)-(23)执行的操作相当于_current_task = next。不再详细解释。

  (24)-(28)执行的是装入新进程上下文的准备工作，也就是准备装入next了。
(24)-(25)是装入next进程的内核和用户栈。因为进程的上下文都保存在该进程
的内核栈里面，所以第一步就是装入该进程的栈指针。(27)-(28)是装入next进
程在切换前的状态信息。(26)就是更新ss1,现在要装入新进程了，当然就要设置
新的用户栈。

  (29)是装入next进程的上下文。next进程在栈里有一个和上图一样的上下文，
现在就要装入。

  (30)是函数调用返回。如果这个进程是刚fork出来的子进程，则上下文里面
r15=ref_from_fork（可以参看copy_thread函数），否则就是返回到switch_to里
面第(6)句位置。

  上面就是进程切换的部分。这部分是和平台相关的。以上是我实现的代码，
感觉效率并不是非常高，但功能是正确的。可能有些地方我没有讲得很清楚，有
什么问题欢迎提出。
--

新的用户栈。

  (29)是装入next进程的上下文。next进程在栈里有一个和上图一样的上下文，
现在就要装入。

  (30)是函数调用返回。如果这个进程是刚fork出来的子进程，则上下文里面
r15=ref_from_fork（可以参看copy_thread函数），否则就是返回到switch_to里
面第(6)句位置。

  上面就是进程切换的部分。这部分是和平台相关的。以上是我实现的代码，
感觉效率并不是非常高，但功能是正确的。可能有些地方我没有讲得很清楚，有
什么问题欢迎提出。




]]></description>
<pubDate>
2006-06-05 15:03:55.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307568.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307568.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[automake/autoconf入门]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307764.shtml</link>
<description>
<![CDATA[作为Linux下的程序开发人员，大家一定都遇到过Makefile，用make命令来编译自己写的程序确实是很方便。一般情况下，大家都是手工写一个简单Makefile，如果要想写出一个符合自由软件惯例的Makefile就不那么容易了。 

　　在本文中，将给大家介绍如何使用autoconf和automake两个工具来帮助我们自动地生成符合自由软件惯例的Makefile，这样就可以象常见的GNU程序一样，只要使用“./configure”，“make”，“make instal”就可以把程序安装到Linux系统中去了。这将特别适合想做开放源代码软件的程序开发人员，又或如果你只是自己写些小的Toy程序，那么这个文章对你也会有很大的帮助。

　　一、Makefile介绍

　　Makefile是用于自动编译和链接的，一个工程有很多文件组成，每一个文件的改变都会导致工程的重新链接，但是不是所有的文件都需要重新编译，Makefile中纪录有文件的信息，在make时会决定在链接的时候需要重新编译哪些文件。

　　Makefile的宗旨就是：让编译器知道要编译一个文件需要依赖其他的哪些文件。当那些依赖文件有了改变，编译器会自动的发现最终的生成文件已经过时，而重新编译相应的模块。

　　Makefile的基本结构不是很复杂，但当一个程序开发人员开始写Makefile时，经常会怀疑自己写的是否符合惯例，而且自己写的Makefile经常和自己的开发环境相关联，当系统环境变量或路径发生了变化后，Makefile可能还要跟着修改。这样就造成了手工书写Makefile的诸多问题，automake恰好能很好地帮助我们解决这些问题。

　　使用automake，程序开发人员只需要写一些简单的含有预定义宏的文件，由autoconf根据一个宏文件生成configure，由automake根据另一个宏文件生成Makefile.in，再使用configure依据Makefile.in来生成一个符合惯例的Makefile。下面我们将详细介绍Makefile的automake生成方法。

　　二、使用的环境

　　本文所提到的程序是基于Linux发行版本：Fedora Core release 1，它包含了我们要用到的autoconf，automake。

　　三、从helloworld入手

　　我们从大家最常使用的例子程序helloworld开始。

　　下面的过程如果简单地说来就是：

　　新建三个文件：

　　　helloworld.c
　　　configure.in
　　　Makefile.am

　　然后执行：

aclocal; autoconf; automake --add-missing; ./configure; make; ./helloworld 

　　就可以看到Makefile被产生出来，而且可以将helloworld.c编译通过。

　　很简单吧，几条命令就可以做出一个符合惯例的Makefile，感觉如何呀。

　　现在开始介绍详细的过程：

　　1、建目录

　　在你的工作目录下建一个helloworld目录，我们用它来存放helloworld程序及相关文件，如在/home/my/build下：

＄ mkdir helloword
＄ cd helloworld 

　　2、 helloworld.c

　　然后用你自己最喜欢的编辑器写一个hellowrold.c文件，如命令：vi helloworld.c。使用下面的代码作为helloworld.c的内容。

int main(int argc, char** argv)
{
printf("Hello, Linux World!\n");
return 0;
} 

　　完成后保存退出。

　　现在在helloworld目录下就应该有一个你自己写的helloworld.c了。

　　3、生成configure

　　我们使用autoscan命令来帮助我们根据目录下的源代码生成一个configure.in的模板文件。

　　命令：

＄ autoscan
＄ ls
configure.scan helloworld.c 

　　执行后在hellowrold目录下会生成一个文件：configure.scan，我们可以拿它作为configure.in的蓝本。

　　现在将configure.scan改名为configure.in，并且编辑它，按下面的内容修改，去掉无关的语句：

============================configure.in内容开始=========================================
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_INIT(helloworld.c)
AM_INIT_AUTOMAKE(helloworld, 1.0)

# Checks for programs.
AC_PROG_CC

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.
AC_OUTPUT(Makefile)
============================configure.in内容结束========================================= 

　　然后执行命令aclocal和autoconf，分别会产生aclocal.m4及configure两个文件：

＄ aclocal 
＄ls 
aclocal.m4 configure.in helloworld.c 
＄ autoconf 
＄ ls 
aclocal.m4 autom4te.cache configure configure.in helloworld.c 


　　大家可以看到configure.in内容是一些宏定义，这些宏经autoconf处理后会变成检查系统特性、环境变量、软件必须的参数的shell脚本。

　　autoconf 是用来生成自动配置软件源代码脚本（configure）的工具。configure脚本能独立于autoconf运行，且在运行的过程中，不需要用户的干预。

　　要生成configure文件，你必须告诉autoconf如何找到你所用的宏。方式是使用aclocal程序来生成你的aclocal.m4。

　　aclocal根据configure.in文件的内容，自动生成aclocal.m4文件。aclocal是一个perl 脚本程序，它的定义是：“aclocal - create aclocal.m4 by scanning configure.ac”。

　　autoconf从configure.in这个列举编译软件时所需要各种参数的模板文件中创建configure。

　　autoconf需要GNU m4宏处理器来处理aclocal.m4，生成configure脚本。

　　m4是一个宏处理器。将输入拷贝到输出，同时将宏展开。宏可以是内嵌的，也可以是用户定义的。除了可以展开宏，m4还有一些内建的函数，用来引用文件，执行命令，整数运算，文本操作，循环等。m4既可以作为编译器的前端，也可以单独作为一个宏处理器。

4、新建Makefile.am

　　新建Makefile.am文件，命令：


＄ vi Makefile.am 


　　内容如下:


AUTOMAKE_OPTIONS=foreign
bin_PROGRAMS=helloworld
helloworld_SOURCES=helloworld.c 


　　automake会根据你写的Makefile.am来自动生成Makefile.in。

　　Makefile.am中定义的宏和目标,会指导automake生成指定的代码。例如，宏bin_PROGRAMS将导致编译和连接的目标被生成。

　　5、运行automake

　　命令：


＄ automake --add-missing
configure.in: installing `./install-sh'
configure.in: installing `./mkinstalldirs'
configure.in: installing `./missing'
Makefile.am: installing `./depcomp' 


　　automake会根据Makefile.am文件产生一些文件，包含最重要的Makefile.in。

　　6、执行configure生成Makefile


＄ ./configure 
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for gawk... gawk
checking whether make sets ＄(MAKE)... yes
checking for gcc... gcc
checking for C compiler default output... a.out
checking whether the C compiler works... yes
checking whether we are cross compiling... no
checking for suffix of executables... 
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ANSI C... none needed
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
configure: creating ./config.status
config.status: creating Makefile
config.status: executing depfiles commands
＄ ls -l Makefile
-rw-rw-r-- 1 yutao yutao 15035 Oct 15 10:40 Makefile 


你可以看到，此时Makefile已经产生出来了。

7、使用Makefile编译代码

 

＄ make
if gcc -DPACKAGE_NAME="" -DPACKAGE_TARNAME="" -DPACKAGE_VERSION="" -

DPACKAGE_STRING="" -DPACKAGE_BUGREPORT="" -DPACKAGE="helloworld" -DVERSION="1.0" 

-I. -I. -g -O2 -MT helloworld.o -MD -MP -MF ".deps/helloworld.Tpo" \
-c -o helloworld.o `test -f 'helloworld.c' || echo './'`helloworld.c; \
then mv -f ".deps/helloworld.Tpo" ".deps/helloworld.Po"; \
else rm -f ".deps/helloworld.Tpo"; exit 1; \
fi
gcc -g -O2 -o helloworld helloworld.o  


　　运行helloworld

 

＄ ./helloworld 
Hello, Linux World! 


　　这样helloworld就编译出来了，你如果按上面的步骤来做的话，应该也会很容易地编译出正确的helloworld文件。你还可以试着使用一些其他的make命令，如make clean，make install，make dist，看看它们会给你什么样的效果。感觉如何？自己也能写出这么专业的Makefile，老板一定会对你刮目相看。

　　四、深入浅出

　　针对上面提到的各个命令，我们再做些详细的介绍。

　　1、 autoscan

　　autoscan是用来扫描源代码目录生成configure.scan文件的。autoscan可以用目录名做为参数，但如果你不使用参数的话，那么autoscan将认为使用的是当前目录。autoscan将扫描你所指定目录中的源文件，并创建configure.scan文件。

　　2、 configure.scan

　　configure.scan包含了系统配置的基本选项，里面都是一些宏定义。我们需要将它改名为configure.in

　　3、 aclocal

　　aclocal是一个perl 脚本程序。aclocal根据configure.in文件的内容，自动生成aclocal.m4文件。aclocal的定义是：“aclocal - create aclocal.m4 by scanning configure.ac”。

　　4、 autoconf

　　autoconf是用来产生configure文件的。configure是一个脚本，它能设置源程序来适应各种不同的操作系统平台，并且根据不同的系统来产生合适的Makefile，从而可以使你的源代码能在不同的操作系统平台上被编译出来。

　　configure.in文件的内容是一些宏，这些宏经过autoconf 处理后会变成检查系统特性、环境变量、软件必须的参数的shell脚本。configure.in文件中的宏的顺序并没有规定，但是你必须在所有宏的最前面和最后面分别加上AC_INIT宏和AC_OUTPUT宏。

　　在configure.ini中：

　　#号表示注释，这个宏后面的内容将被忽略。

　　AC_INIT(FILE) 

　　这个宏用来检查源代码所在的路径。

AM_INIT_AUTOMAKE(PACKAGE, VERSION)  

　　 这个宏是必须的，它描述了我们将要生成的软件包的名字及其版本号：PACKAGE是软件包的名字，VERSION是版本号。当你使用make dist命令时，它会给你生成一个类似helloworld-1.0.tar.gz的软件发行包，其中就有对应的软件包的名字和版本号。

AC_PROG_CC

　　这个宏将检查系统所用的C编译器。 

AC_OUTPUT(FILE)

　　这个宏是我们要输出的Makefile的名字。

　　我们在使用automake时，实际上还需要用到其他的一些宏，但我们可以用aclocal 来帮我们自动产生。执行aclocal后我们会得到aclocal.m4文件。

　　产生了configure.in和aclocal.m4 两个宏文件后，我们就可以使用autoconf来产生configure文件了。

　　5、 Makefile.am

　　Makefile.am是用来生成Makefile.in的，需要你手工书写。Makefile.am中定义了一些内容：

AUTOMAKE_OPTIONS 

　　这个是automake的选项。在执行automake时，它会检查目录下是否存在标准GNU软件包中应具备的各种文件，例如AUTHORS、ChangeLog、NEWS等文件。我们将其设置成foreign时，automake会改用一般软件包的标准来检查。

bin_PROGRAMS

　　这个是指定我们所要产生的可执行文件的文件名。如果你要产生多个可执行文件，那么在各个名字间用空格隔开。 

helloworld_SOURCES 

　　这个是指定产生“helloworld”时所需要的源代码。如果它用到了多个源文件，那么请使用空格符号将它们隔开。比如需要helloworld.h，helloworld.c那么请写成helloworld_SOURCES= helloworld.h helloworld.c。

　　如果你在bin_PROGRAMS定义了多个可执行文件，则对应每个可执行文件都要定义相对的filename_SOURCES。

　　6、 automake

　　我们使用automake --add-missing来产生Makefile.in。

　　选项--add-missing的定义是“add missing standard files to package”，它会让automake加入一个标准的软件包所必须的一些文件。

　　我们用automake产生出来的Makefile.in文件是符合GNU Makefile惯例的，接下来我们只要执行configure这个shell 脚本就可以产生合适的 Makefile 文件了。

　　7、 Makefile

　　在符合GNU Makefiel惯例的Makefile中，包含了一些基本的预先定义的操作：

make

　　根据Makefile编译源代码，连接，生成目标文件，可执行文件。

make clean

　　清除上次的make命令所产生的object文件（后缀为“.o”的文件）及可执行文件。

make install

　　将编译成功的可执行文件安装到系统目录中，一般为/usr/local/bin目录。

make dist

　　产生发布软件包文件（即distribution package）。这个命令将会将可执行文件及相关文件打包成一个tar.gz压缩的文件用来作为发布软件的软件包。

　　它会在当前目录下生成一个名字类似“PACKAGE-VERSION.tar.gz”的文件。PACKAGE和VERSION，是我们在configure.in中定义的AM_INIT_AUTOMAKE(PACKAGE, VERSION)。

make distcheck

　　生成发布软件包并对其进行测试检查，以确定发布包的正确性。这个操作将自动把压缩包文件解开，然后执行configure命令，并且执行make，来确认编译不出现错误，最后提示你软件包已经准备好，可以发布了。

===============================================
helloworld-1.0.tar.gz is ready for distribution
===============================================
make distclean 

　　类似make clean，但同时也将configure生成的文件全部删除掉，包括Makefile。

　　五、结束语

　　通过上面的介绍，你应该可以很容易地生成一个你自己的符合GNU惯例的Makefile文件及对应的项目文件。

　　如果你想写出更复杂的且符合惯例的Makefile，你可以参考一些开放代码的项目中的configure.in和Makefile.am文件，比如：嵌入式数据库sqlite，单元测试cppunit。 

]]></description>
<pubDate>
2006-05-04 16:33:03.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307764.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307764.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[windows进程中的内存结构]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102307830.shtml</link>
<description>
<![CDATA[在阅读本文之前，如果你连堆栈是什么多不知道的话，请先阅读文章后面的基础知识。

接触过编程的人都知道，高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢？程序又是如何使用这些变量的呢？下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明，默认都使用VC编译的release版。

首先，来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local)，静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码：

#include <stdio.h>

int g1=0, g2=0, g3=0;

int main()
{
static int s1=0, s2=0, s3=0;
int v1=0, v2=0, v3=0;

//打印出各个变量的内存地址

printf("0x%08x\n",&v1); //打印各本地变量的内存地址
printf("0x%08x\n",&v2);
printf("0x%08x\n\n",&v3);
printf("0x%08x\n",&g1); //打印各全局变量的内存地址
printf("0x%08x\n",&g2);
printf("0x%08x\n\n",&g3);
printf("0x%08x\n",&s1); //打印各静态变量的内存地址
printf("0x%08x\n",&s2);
printf("0x%08x\n\n",&s3);
return 0;
}

编译后的执行结果是：

0x0012ff78
0x0012ff7c
0x0012ff80

0x004068d0
0x004068d4
0x004068d8

0x004068dc
0x004068e0
0x004068e4

输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量，g1,g2,g3是全局变量，s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的，但是本地变量和全局变量分配的内存地址差了十万八千里，而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言，可以在逻辑上分成3个部份：代码区，静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区，栈是一种线性结构，堆是一种链式结构。进程的每个线程都有私有的“栈”，所以每个线程虽然代码一样，但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区，本地变量分配在动态数据区，即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。


├———————┤低端内存区域
│ …… │
├———————┤
│ 动态数据区 │
├———————┤
│ …… │
├———————┤
│ 代码区 │
├———————┤
│ 静态数据区 │
├———————┤
│ …… │
├———————┤高端内存区域


堆栈是一个先进后出的数据结构，栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程，以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定，这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的，前者由被调函数调整堆栈，后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码：

#include <stdio.h>

void __stdcall func(int param1,int param2,int param3)
{
int var1=param1;
int var2=param2;
int var3=param3;
printf("0x%08x\n",&para;m1); //打印出各个变量的内存地址
printf("0x%08x\n",&para;m2);
printf("0x%08x\n\n",&para;m3);
printf("0x%08x\n",&var1);
printf("0x%08x\n",&var2);
printf("0x%08x\n\n",&var3);
return;
}

int main()
{
func(1,2,3);
return 0;
}

编译后的执行结果是：

0x0012ff78
0x0012ff7c
0x0012ff80

0x0012ff68
0x0012ff6c
0x0012ff70



├———————┤<—函数执行时的栈顶（ESP）、低端内存区域
│ …… │
├———————┤
│ var 1 │
├———————┤
│ var 2 │
├———————┤
│ var 3 │
├———————┤
│ RET │
├———————┤<—“__cdecl”函数返回后的栈顶（ESP）
│ parameter 1 │
├———————┤
│ parameter 2 │
├———————┤
│ parameter 3 │
├———————┤<—“__stdcall”函数返回后的栈顶（ESP）
│ …… │
├———————┤<—栈底（基地址 EBP）、高端内存区域


上图就是函数调用过程中堆栈的样子了。首先，三个参数以从又到左的次序压入堆栈，先压“param3”，再压“param2”，最后压入“param1”；然后压入函数的返回地址(RET)，接着跳转到函数地址接着执行（这里要补充一点，介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后，继续压入当前EBP，然后用当前ESP代替EBP。然而，有一篇介绍windows下函数调用的文章中说，在windows下的函数调用也有这一步骤，但根据我的实际调试，并未发现这一步，这还可以从param3和var1之间只有4字节的间隙这点看出来）；第三步，将栈顶(ESP)减去一个数，为本地变量分配内存空间，上例中是减去12字节(ESP=ESP-3*4，每个int变量占用4个字节)；接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈，所以在函数返回前要恢复堆栈，先回收本地变量占用的内存(ESP=ESP+3*4)，然后取出返回地址，填入EIP寄存器，回收先前压入参数占用的内存(ESP=ESP+3*4)，继续执行调用者的代码。参见下列汇编代码：

;--------------func 函数的汇编代码-------------------

:00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间
:00401003 8B442410 mov eax, dword ptr [esp+10]
:00401007 8B4C2414 mov ecx, dword ptr [esp+14]
:0040100B 8B542418 mov edx, dword ptr [esp+18]
:0040100F 89442400 mov dword ptr [esp], eax
:00401013 8D442410 lea eax, dword ptr [esp+10]
:00401017 894C2404 mov dword ptr [esp+04], ecx

……………………（省略若干代码）

:00401075 83C43C add esp, 0000003C ;恢复堆栈，回收本地变量的内存空间
:00401078 C3 ret 000C ;函数返回，恢复参数占用的内存空间
;如果是“__cdecl”的话，这里是“ret”，堆栈将由调用者恢复

;-------------------函数结束-------------------------


;--------------主程序调用func函数的代码--------------

:00401080 6A03 push 00000003 //压入参数param3
:00401082 6A02 push 00000002 //压入参数param2
:00401084 6A01 push 00000001 //压入参数param1
:00401086 E875FFFFFF call 00401000 //调用func函数
;如果是“__cdecl”的话，将在这里恢复堆栈，“add esp, 0000000C”

聪明的读者看到这里，差不多就明白缓冲溢出的原理了。先来看下面的代码：

#include <stdio.h>
#include <string.h>

void __stdcall func()
{
char lpBuff[8]="\0";
strcat(lpBuff,"AAAAAAAAAAA");
return;
}

int main()
{
func();
return 0;
}

编译后执行一下回怎么样？哈，“"0x00414141"指令引用的"0x00000000"内存。该内存不能为"read"。”，“非法操作”喽！"41"就是"A"的16进制的ASCII码了，那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节，算进结尾的'\0'，那strcat最多只能写入7个"A"，但程序实际写入了11个"A"外加1个'\0'。再来看看上面那幅图，多出来的4个字节正好覆盖了RET的所在的内存空间，导致函数返回到一个错误的内存地址，执行了错误的指令。如果能精心构造这个字符串，使它分成三部分，前一部份仅仅是填充的无意义数据以达到溢出的目的，接着是一个覆盖RET的数据，紧接着是一段shellcode，那只要着个RET地址能指向这段shellcode的第一个指令，那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置，那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令，使得exploit有更强的通用性。


├———————┤<—低端内存区域
│ …… │
├———————┤<—由exploit填入数据的开始
│ │
│ buffer │<—填入无用的数据
│ │
├———————┤
│ RET │<—指向shellcode，或NOP指令的范围
├———————┤
│ NOP │
│ …… │<—填入的NOP指令，是RET可指向的范围
│ NOP │
├———————┤
│ │
│ shellcode │
│ │
├———————┤<—由exploit填入数据的结束
│ …… │
├———————┤<—高端内存区域


windows下的动态数据除了可存放在栈中，还可以存放在堆中。了解C++的朋友都知道，C++可以使用new关键字来动态分配内存。来看下面的C++代码：

#include <stdio.h>
#include <iostream.h>
#include <windows.h>

void func()
{
char *buffer=new char[128];
char bufflocal[128];
static char buffstatic[128];
printf("0x%08x\n",buffer); //打印堆中变量的内存地址
printf("0x%08x\n",bufflocal); //打印本地变量的内存地址
printf("0x%08x\n",buffstatic); //打印静态变量的内存地址
}

void main()
{
func();
return;
}

程序执行结果为：

0x004107d0
0x0012ff04
0x004068c0

可以发现用new关键字分配的内存即不在栈中，也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前，先来了解一下和“堆”有关的几个API函数：

HeapAlloc 在堆中申请内存空间 
HeapCreate 创建一个新的堆对象
HeapDestroy 销毁一个堆对象
HeapFree 释放申请的内存
HeapWalk 枚举堆对象的所有内存块
GetProcessHeap 取得进程的默认堆对象
GetProcessHeaps 取得进程所有的堆对象
LocalAlloc
GlobalAlloc

当进程初始化时，系统会自动为进程创建一个默认堆，这个堆默认所占内存的大小为1M。堆对象由系统进行管理，它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间：

HANDLE hHeap=GetProcessHeap();
char *buff=HeapAlloc(hHeap,0,8);

其中hHeap是堆对象的句柄，buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢？它的值有什么意义吗？看看下面这段代码吧：

#pragma comment(linker,"/entry:main") //定义程序的入口
#include <windows.h>

_CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf
/*---------------------------------------------------------------------------
写到这里，我们顺便来复习一下前面所讲的知识：
(*注)printf函数是C语言的标准函数库中函数，VC的标准函数库由msvcrt.dll模块实现。
由函数定义可见，printf的参数个数是可变的，函数内部无法预先知道调用者压入的参数个数，函数只能通过分析第一个参数字符串的格式来获得压入参数的信息，由于这里参数的个数是动态的，所以必须由调用者来平衡堆栈，这里便使用了__cdecl调用规则。BTW，Windows系统的API函数基本上是__stdcall调用形式，只有一个API例外，那就是wsprintf，它使用__cdecl调用规则，同printf函数一样，这是由于它的参数个数是可变的缘故。
---------------------------------------------------------------------------*/
void main()
{
HANDLE hHeap=GetProcessHeap();
char *buff=HeapAlloc(hHeap,0,0x10);
char *buff2=HeapAlloc(hHeap,0,0x10);
HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");
printf=(void *)GetProcAddress(hMsvcrt,"printf");
printf("0x%08x\n",hHeap);
printf("0x%08x\n",buff);
printf("0x%08x\n\n",buff2);
}

执行结果为：

0x00130000
0x00133100
0x00133118

hHeap的值怎么和那个buff的值那么接近呢？其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构，这个结构中存放着一些有关进程的重要信息，其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址，而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据，如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的，同一时刻只能有一个线程访问堆中的数据，当多个线程同时有访问要求时，只能排队等待，这样便造成程序执行效率下降。

最后来说说内存中的数据对齐。所位数据对齐，是指数据所在的内存地址必须是该数据长度的整数倍，DWORD数据的内存起始地址能被4除尽，WORD数据的内存起始地址能被2除尽，x86 CPU能直接访问对齐的数据，当他试图访问一个未对齐的数据时，会在内部进行一系列的调整，这些调整对于程序来说是透明的，但是会降低运行速度，所以编译器在编译程序时会尽量保证数据对齐。同样一段代码，我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果：

#include <stdio.h>

int main()
{
int a;
char b;
int c;
printf("0x%08x\n",&a);
printf("0x%08x\n",&b);
printf("0x%08x\n",&c);
return 0;
}

这是用VC编译后的执行结果：
0x0012ff7c
0x0012ff7b
0x0012ff80
变量在内存中的顺序：b(1字节)-a(4字节)-c(4字节)。

这是用Dev-C++编译后的执行结果：
0x0022ff7c
0x0022ff7b
0x0022ff74
变量在内存中的顺序：c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。

这是用lcc编译后的执行结果：
0x0012ff6c
0x0012ff6b
0x0012ff64
变量在内存中的顺序：同上。

三个编译器都做到了数据对齐，但是后两个编译器显然没VC“聪明”，让一个char占了4字节，浪费内存哦。








基础知识：
堆栈是一种简单的数据结构，是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶，另一端称为栈底，对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中，POP指令实现出栈操作，PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针，EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址，当CPU执行完当前的指令后，从EIP寄存器中读取下一条指令的内存地址，然后继续执行。


参考：《Windows下的HEAP溢出及其利用》by: isno
《windows核心编程》by: Jeffrey Richter
===============================================
本文版权属20CN网络安全小组及其作者所有,如有转载,请保持文章完整性并注明出处
]]></description>
<pubDate>
2006-04-26 11:09:55.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102307830.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102307830.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[Makefile学习教程]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308044.shtml</link>
<description>
<![CDATA[0 Makefile概述 
________________________________________
什么是makefile？或许很多Winodws的程序员都不知道这个东西，因为那些Windows的IDE都为你做了这个工作，但我觉得要作一个好的和professional的程序员，makefile还是要懂。这就好像现在有这么多的HTML的编辑器，但如果你想成为一个专业人士，你还是要了解HTML的标识的含义。特别在Unix下的软件编译，你就不能不自己写makefile了，会不会写makefile，从一个侧面说明了一个人是否具备完成大型工程的能力。 
因为，makefile关系到了整个工程的编译规则。一个工程中的源文件不计数，其按类型、功能、模块分别放在若干个目录中，makefile定义了一系列的规则来指定，哪些文件需要先编译，哪些文件需要后编译，哪些文件需要重新编译，甚至于进行更复杂的功能操作，因为makefile就像一个Shell脚本一样，其中也可以执行操作系统的命令。 
makefile带来的好处就是——“自动化编译”，一旦写好，只需要一个make命令，整个工程完全自动编译，极大的提高了软件开发的效率。make是一个命令工具，是一个解释makefile中指令的命令工具，一般来说，大多数的IDE都有这个命令，比如：Delphi的make，Visual C++的nmake，Linux下GNU的make。可见，makefile都成为了一种在工程方面的编译方法。 
现在讲述如何写makefile的文章比较少，这是我想写这篇文章的原因。当然，不同产商的make各不相同，也有不同的语法，但其本质都是在“文件依赖性”上做文章，这里，我仅对GNU的make进行讲述，我的环境是RedHat Linux 8.0，make的版本是3.80。必竟，这个make是应用最为广泛的，也是用得最多的。而且其还是最遵循于IEEE 1003.2-1992 标准的（POSIX.2）。 
在这篇文档中，将以C/C++的源码作为我们基础，所以必然涉及一些关于C/C++的编译的知识，相关于这方面的内容，还请各位查看相关的编译器的文档。这里所默认的编译器是UNIX下的GCC和CC。 
0.1 关于程序的编译和链接 
在此，我想多说关于程序编译的一些规范和方法，一般来说，无论是C、C++、还是pas，首先要把源文件编译成中间代码文件，在Windows下也就是 .obj 文件，UNIX下是 .o 文件，即 Object File，这个动作叫做编译（compile）。然后再把大量的Object File合成执行文件，这个动作叫作链接（link）。 
编译时，编译器需要的是语法的正确，函数与变量的声明的正确。对于后者，通常是你需要告诉编译器头文件的所在位置（头文件中应该只是声明，而定义应该放在C/C++文件中），只要所有的语法正确，编译器就可以编译出中间目标文件。一般来说，每个源文件都应该对应于一个中间目标文件（O文件或是OBJ文件）。 
链接时，主要是链接函数和全局变量，所以，我们可以使用这些中间目标文件（O文件或是OBJ文件）来链接我们的应用程序。链接器并不管函数所在的源文件，只管函数的中间目标文件（Object File），在大多数时候，由于源文件太多，编译生成的中间目标文件太多，而在链接时需要明显地指出中间目标文件名，这对于编译很不方便，所以，我们要给中间目标文件打个包，在Windows下这种包叫“库文件”（Library File)，也就是 .lib 文件，在UNIX下，是Archive File，也就是 .a 文件。 
总结一下，源文件首先会生成中间目标文件，再由中间目标文件生成执行文件。在编译时，编译器只检测程序语法，和函数、变量是否被声明。如果函数未被声明，编译器会给出一个警告，但可以生成Object File。而在链接程序时，链接器会在所有的Object File中找寻函数的实现，如果找不到，那到就会报链接错误码（Linker Error），在VC下，这种错误一般是：Link 2001错误，意思说是说，链接器未能找到函数的实现。你需要指定函数的Object File. 
好，言归正传，GNU的make有许多的内容，闲言少叙，还是让我们开始吧。 
1 Makefile 介绍 
________________________________________
make命令执行时，需要一个 Makefile 文件，以告诉make命令需要怎么样的去编译和链接程序。 
首先，我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册，在这个示例中，我们的工程有8个C文件，和3个头文件，我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是： 
1.如果这个工程没有编译过，那么我们的所有C文件都要编译并被链接。 
2.如果这个工程的某几个C文件被修改，那么我们只编译被修改的C文件，并链接目标程序。 
3.如果这个工程的头文件被改变了，那么我们需要编译引用了这几个头文件的C文件，并链接目标程序。 
只要我们的Makefile写得够好，所有的这一切，我们只用一个make命令就可以完成，make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译，从而自己编译所需要的文件和链接目标程序。 
1.1 Makefile的规则 
在讲述这个Makefile之前，还是让我们先来粗略地看一看Makefile的规则。 
target ... : prerequisites ...
   command
   ...
   ...
target也就是一个目标文件，可以是Object File，也可以是执行文件。还可以是一个标签（Label），对于标签这种特性，在后续的“伪目标”章节中会有叙述。 
prerequisites就是，要生成那个target所需要的文件或是目标。 
command也就是make需要执行的命令。（任意的Shell命令） 
这是一个文件的依赖关系，也就是说，target这一个或多个的目标文件依赖于prerequisites中的文件，其生成规则定义在command中。说白一点就是说，prerequisites中如果有一个以上的文件比target文件要新的话，command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。 
说到底，Makefile的东西就是这样一点，好像我的这篇文档也该结束了。呵呵。还不尽然，这是Makefile的主线和核心，但要写好一个Makefile还不够，我会以后面一点一点地结合我的工作经验给你慢慢到来。内容还多着呢。：） 
1.2 一个示例 
正如前面所说的，如果一个工程有3个头文件，和8个C文件，我们为了完成前面所述的那三个规则，我们的Makefile应该是下面的这个样子的。 
    edit : main.o kbd.o command.o display.o            insert.o search.o files.o utils.o
            cc -o edit main.o kbd.o command.o display.o                        insert.o search.o files.o utils.o

    main.o : main.c defs.h
            cc -c main.c
    kbd.o : kbd.c defs.h command.h
            cc -c kbd.c
    command.o : command.c defs.h command.h
            cc -c command.c
    display.o : display.c defs.h buffer.h
            cc -c display.c
    insert.o : insert.c defs.h buffer.h
            cc -c insert.c
    search.o : search.c defs.h buffer.h
            cc -c search.c
    files.o : files.c defs.h buffer.h command.h
            cc -c files.c
    utils.o : utils.c defs.h
            cc -c utils.c
    clean :
            rm edit main.o kbd.o command.o display.o                insert.o search.o files.o utils.o
反斜杠（\）是换行符的意思。这样比较便于Makefile的易读。我们可以把这个内容保存在文件为“Makefile”或“makefile”的文件中，然后在该目录下直接输入命令“make”就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件，那么，只要简单地执行一下“make clean”就可以了。 
在这个makefile中，目标文件（target）包含：执行文件edit和中间目标文件（*.o），依赖文件（prerequisites）就是冒号后面的那些 .c 文件和 .h文件。每一个 .o 文件都有一组依赖文件，而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的，换言之，目标文件是哪些文件更新的。 
在定义好依赖关系后，后续的那一行定义了如何生成目标文件的操作系统命令，一定要以一个Tab键作为开头。记住，make并不管命令是怎么工作的，他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期，如果prerequisites文件的日期要比targets文件的日期要新，或者target不存在的话，那么，make就会执行后续定义的命令。 
这里要说明一点的是，clean不是一个文件，它只不过是一个动作名字，有点像C语言中的lable一样，其冒号后什么也没有，那么，make就不会自动去找文件的依赖性，也就不会自动执行其后所定义的命令。要执行其后的命令，就要在make命令后明显得指出这个lable的名字。这样的方法非常有用，我们可以在一个makefile中定义不用的编译或是和编译无关的命令，比如程序的打包，程序的备份，等等。 
1.3 make是如何工作的 
在默认的方式下，也就是我们只输入make命令。那么， 
1.make会在当前目录下找名字叫“Makefile”或“makefile”的文件。 
2.如果找到，它会找文件中的第一个目标文件（target），在上面的例子中，他会找到“edit”这个文件，并把这个文件作为最终的目标文件。 
3.如果edit文件不存在，或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新，那么，他就会执行后面所定义的命令来生成edit这个文件。 
4.如果edit所依赖的.o文件也存在，那么make会在当前文件中找目标为.o文件的依赖性，如果找到则再根据那一个规则生成.o文件。（这有点像一个堆栈的过程） 
5.当然，你的C文件和H文件是存在的啦，于是make会生成 .o 文件，然后再用 .o 文件生命make的终极任务，也就是执行文件edit了。 
这就是整个make的依赖性，make会一层又一层地去找文件的依赖关系，直到最终编译出第一个目标文件。在找寻的过程中，如果出现错误，比如最后被依赖的文件找不到，那么make就会直接退出，并报错，而对于所定义的命令的错误，或是编译不成功，make根本不理。make只管文件的依赖性，即，如果在我找了依赖关系之后，冒号后面的文件还是不在，那么对不起，我就不工作啦。 
通过上述分析，我们知道，像clean这种，没有被第一个目标文件直接或间接关联，那么它后面所定义的命令将不会被自动执行，不过，我们可以显示要make执行。即命令——“make clean”，以此来清除所有的目标文件，以便重编译。 
于是在我们编程中，如果这个工程已被编译过了，当我们修改了其中一个源文件，比如file.c，那么根据我们的依赖性，我们的目标file.o会被重编译（也就是在这个依性关系后面所定义的命令），于是file.o的文件也是最新的啦，于是file.o的文件修改时间要比edit要新，所以edit也会被重新链接了（详见edit目标文件后定义的命令）。 
而如果我们改变了“command.h”，那么，kdb.o、command.o和files.o都会被重编译，并且，edit会被重链接。 
1.4 makefile中使用变量 
在上面的例子中，先让我们看看edit的规则： 
      edit : main.o kbd.o command.o display.o                   insert.o search.o files.o utils.o
            cc -o edit main.o kbd.o command.o display.o                        insert.o search.o files.o utils.o
我们可以看到[.o]文件的字符串被重复了两次，如果我们的工程需要加入一个新的[.o]文件，那么我们需要在两个地方加（应该是三个地方，还有一个地方在clean中）。当然，我们的makefile并不复杂，所以在两个地方加也不累，但如果makefile变得复杂，那么我们就有可能会忘掉一个需要加入的地方，而导致编译失败。所以，为了makefile的易维护，在makefile中我们可以使用变量。makefile的变量也就是一个字符串，理解成C语言中的宏可能会更好。 
比如，我们声明一个变量，叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ，反正不管什么啦，只要能够表示obj文件就行了。我们在makefile一开始就这样定义： 
     objects = main.o kbd.o command.o display.o               insert.o search.o files.o utils.o
于是，我们就可以很方便地在我们的makefile中以“＄(objects)”的方式来使用这个变量了，于是我们的改良版makefile就变成下面这个样子： 
    objects = main.o kbd.o command.o display.o               insert.o search.o files.o utils.o

    edit : ＄(objects)
            cc -o edit ＄(objects)
    main.o : main.c defs.h
            cc -c main.c
    kbd.o : kbd.c defs.h command.h
            cc -c kbd.c
    command.o : command.c defs.h command.h
            cc -c command.c
    display.o : display.c defs.h buffer.h
            cc -c display.c
    insert.o : insert.c defs.h buffer.h
            cc -c insert.c
    search.o : search.c defs.h buffer.h
            cc -c search.c
    files.o : files.c defs.h buffer.h command.h
            cc -c files.c
    utils.o : utils.c defs.h
            cc -c utils.c
    clean :
            rm edit ＄(objects)
于是如果有新的 .o 文件加入，我们只需简单地修改一下 objects 变量就可以了。 
关于变量更多的话题，我会在后续给你一一道来。 
1.5 让make自动推导 
GNU的make很强大，它可以自动推导文件以及文件依赖关系后面的命令，于是我们就没必要去在每一个[.o]文件后都写上类似的命令，因为，我们的make会自动识别，并自己推导命令。 
只要make看到一个[.o]文件，它就会自动的把[.c]文件加在依赖关系中，如果make找到一个whatever.o，那么whatever.c，就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来，于是，我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。 
    objects = main.o kbd.o command.o display.o               insert.o search.o files.o utils.o

    edit : ＄(objects)
            cc -o edit ＄(objects)

    main.o : defs.h
    kbd.o : defs.h command.h
    command.o : defs.h command.h
    display.o : defs.h buffer.h
    insert.o : defs.h buffer.h
    search.o : defs.h buffer.h
    files.o : defs.h buffer.h command.h
    utils.o : defs.h

    .PHONY : clean
    clean :
            rm edit ＄(objects)
这种方法，也就是make的“隐晦规则”。上面文件内容中，“.PHONY”表示，clean是个伪目标文件。 
关于更为详细的“隐晦规则”和“伪目标文件”，我会在后续给你一一道来。 
1.6 另类风格的makefile 
即然我们的make可以自动推导命令，那么我看到那堆[.o]和[.h]的依赖就有点不爽，那么多的重复的[.h]，能不能把其收拢起来，好吧，没有问题，这个对于make来说很容易，谁叫它提供了自动推导命令和文件的功能呢？来看看最新风格的makefile吧。 
    objects = main.o kbd.o command.o display.o               insert.o search.o files.o utils.o

    edit : ＄(objects)
            cc -o edit ＄(objects)

    ＄(objects) : defs.h
    kbd.o command.o files.o : command.h
    display.o insert.o search.o files.o : buffer.h

    .PHONY : clean
    clean :
            rm edit ＄(objects)
这种风格，让我们的makefile变得很简单，但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的，一是文件的依赖关系看不清楚，二是如果文件一多，要加入几个新的.o文件，那就理不清楚了。 
1.7 清空目标文件的规则 
每个Makefile中都应该写一个清空目标文件（.o和执行文件）的规则，这不仅便于重编译，也很利于保持文件的清洁。这是一个“修养”（呵呵，还记得我的《编程修养》吗）。一般的风格都是： 
        clean:
            rm edit ＄(objects)
更为稳健的做法是： 
        .PHONY : clean
        clean :
                -rm edit ＄(objects)
前面说过，.PHONY意思表示clean是一个“伪目标”，。而在rm命令前面加了一个小减号的意思就是，也许某些文件出现问题，但不要管，继续做后面的事。当然，clean的规则不要放在文件的开头，不然，这就会变成make的默认目标，相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。 
上面就是一个makefile的概貌，也是makefile的基础，下面还有很多makefile的相关细节，准备好了吗？准备好了就来。 
________________________________________
2 Makefile 总述 
2.1 Makefile里有什么？ 
Makefile里主要包含了五个东西：显式规则、隐晦规则、变量定义、文件指示和注释。 
1.显式规则。显式规则说明了，如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出，要生成的文件，文件的依赖文件，生成的命令。 
2.隐晦规则。由于我们的make有自动推导的功能，所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile，这是由make所支持的。 
3.变量的定义。在Makefile中我们要定义一系列的变量，变量一般都是字符串，这个有点你C语言中的宏，当Makefile被执行时，其中的变量都会被扩展到相应的引用位置上。 
4.文件指示。其包括了三个部分，一个是在一个Makefile中引用另一个Makefile，就像C语言中的include一样；另一个是指根据某些情况指定Makefile中的有效部分，就像C语言中的预编译#if一样；还有就是定义一个多行的命令。有关这一部分的内容，我会在后续的部分中讲述。 
5.注释。Makefile中只有行注释，和UNIX的Shell脚本一样，其注释是用“#”字符，这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符，可以用反斜框进行转义，如：“\#”。 
最后，还值得一提的是，在Makefile中的命令，必须要以[Tab]键开始。 
2.2Makefile的文件名 
默认的情况下，make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件，找到了解释这个文件。在这三个文件名中，最好使用“Makefile”这个文件名，因为，这个文件名第一个字符为大写，这样有一种显目的感觉。最好不要用“GNUmakefile”，这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感，但是基本上来说，大多数的make都支持“makefile”和“Makefile”这两种默认文件名。 
当然，你可以使用别的文件名来书写Makefile，比如：“Make.Linux”，“Make.Solaris”，“Make.AIX”等，如果要指定特定的Makefile，你可以使用make的“-f”和“--file”参数，如：make -f Make.Linux或make --file Make.AIX。 
2.3 引用其它的Makefile 
在Makefile使用include关键字可以把别的Makefile包含进来，这很像C语言的#include，被包含的文件会原模原样的放在当前文件的包含位置。include的语法是： 
include <filename>
filename可以是当前操作系统Shell的文件模式（可以保含路径和通配符） 
在include前面可以有一些空字符，但是绝不能是[Tab]键开始。include和可以用一个或多个空格隔开。举个例子，你有这样几个Makefile：a.mk、b.mk、c.mk，还有一个文件叫foo.make，以及一个变量＄(bar)，其包含了e.mk和f.mk，那么，下面的语句： 
    include foo.make *.mk ＄(bar)
等价于： 
    include foo.make a.mk b.mk c.mk e.mk f.mk
make命令开始时，会把找寻include所指出的其它Makefile，并把其内容安置在当前的位置。就好像C/C++的#include指令一样。如果文件都没有指定绝对路径或是相对路径的话，make会在当前目录下首先寻找，如果当前目录下没有找到，那么，make还会在下面的几个目录下找： 
1.如果make执行时，有“-I”或“--include-dir”参数，那么make就会在这个参数所指定的目录下去寻找。 
2.如果目录/include（一般是：/usr/local/bin或/usr/include）存在的话，make也会去找。 
如果有文件没有找到的话，make会生成一条警告信息，但不会马上出现致命错误。它会继续载入其它的文件，一旦完成makefile的读取，make会再重试这些没有找到，或是不能读取的文件，如果还是不行，make才会出现一条致命信息。如果你想让make不理那些无法读取的文件，而继续执行，你可以在include前加一个减号“-”。如： 
-include <filename>
其表示，无论include过程中出现什么错误，都不要报错继续执行。和其它版本make兼容的相关命令是sinclude，其作用和这一个是一样的。 
2.4 环境变量 MAKEFILES 
如果你的当前环境中定义了环境变量MAKEFILES，那么，make会把这个变量中的值做一个类似于include的动作。这个变量中的值是其它的Makefile，用空格分隔。只是，它和include不同的是，从这个环境变中引入的Makefile的“目标”不会起作用，如果环境变量中定义的文件发现错误，make也会不理。 
但是在这里我还是建议不要使用这个环境变量，因为只要这个变量一被定义，那么当你使用make时，所有的Makefile都会受到它的影响，这绝不是你想看到的。在这里提这个事，只是为了告诉大家，也许有时候你的Makefile出现了怪事，那么你可以看看当前环境中有没有定义这个变量。 
2.5 make的工作方式 
GNU的make工作时的执行步骤入下：（想来其它的make也是类似） 
1.读入所有的Makefile。 
2.读入被include的其它Makefile。 
3.初始化文件中的变量。 
4.推导隐晦规则，并分析所有规则。 
5.为所有的目标文件创建依赖关系链。 
6.根据依赖关系，决定哪些目标要重新生成。 
7.执行生成命令。 
1-5步为第一个阶段，6-7为第二个阶段。第一个阶段中，如果定义的变量被使用了，那么，make会把其展开在使用的位置。但make并不会完全马上展开，make使用的是拖延战术，如果变量出现在依赖关系的规则中，那么仅当这条依赖被决定要使用了，变量才会在其内部展开。 
当然，这个工作方式你不一定要清楚，但是知道这个方式你也会对make更为熟悉。有了这个基础，后续部分也就容易看懂了。 
3 Makefile书写规则 
________________________________________
规则包含两个部分，一个是依赖关系，一个是生成目标的方法。 
在Makefile中，规则的顺序是很重要的，因为，Makefile中只应该有一个最终目标，其它的目标都是被这个目标所连带出来的，所以一定要让make知道你的最终目标是什么。一般来说，定义在Makefile中的目标可能会有很多，但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个，那么，第一个目标会成为最终的目标。make所完成的也就是这个目标。 
好了，还是让我们来看一看如何书写规则。 
3.1 规则举例 
 foo.o : foo.c defs.h       # foo模块
            cc -c -g foo.c
看到这个例子，各位应该不是很陌生了，前面也已说过，foo.o是我们的目标，foo.c和defs.h是目标所依赖的源文件，而只有一个命令“cc -c -g foo.c”（以Tab键开头）。这个规则告诉我们两件事： 
1.文件的依赖关系，foo.o依赖于foo.c和defs.h的文件，如果foo.c和defs.h的文件日期要比foo.o文件日期要新，或是foo.o不存在，那么依赖关系发生。 
2.如果生成（或更新）foo.o文件。也就是那个cc命令，其说明了，如何生成foo.o这个文件。（当然foo.c文件include了defs.h文件） 
3.2 规则的语法 
      targets : prerequisites
        command
        ...
或是这样： 
      targets : prerequisites ; command
            command
            ...
targets是文件名，以空格分开，可以使用通配符。一般来说，我们的目标基本上是一个文件，但也有可能是多个文件。 
command是命令行，如果其不与“target:prerequisites”在一行，那么，必须以[Tab键]开头，如果和prerequisites在一行，那么可以用分号做为分隔。（见上） 
prerequisites也就是目标所依赖的文件（或依赖目标）。如果其中的某个文件要比目标文件要新，那么，目标就被认为是“过时的”，被认为是需要重生成的。这个在前面已经讲过了。 
如果命令太长，你可以使用反斜框（‘\’）作为换行符。make对一行上有多少个字符没有限制。规则告诉make两件事，文件的依赖关系和如何成成目标文件。 
一般来说，make会以UNIX的标准Shell，也就是/bin/sh来执行命令。 
3.3 在规则中使用通配符 
如果我们想定义一系列比较类似的文件，我们很自然地就想起使用通配符。make支持三各通配符：“*”，“?”和“[...]”。这是和Unix的B-Shell是相同的。 
波浪号（“~”）字符在文件名中也有比较特殊的用途。如果是“~/test”，这就表示当前用户的＄HOME目录下的test目录。而“~hchen/test”则表示用户hchen的宿主目录下的test目录。（这些都是Unix下的小知识了，make也支持）而在Windows或是MS-DOS下，用户没有宿主目录，那么波浪号所指的目录则根据环境变量“HOME”而定。 
通配符代替了你一系列的文件，如“*.c”表示所以后缀为c的文件。一个需要我们注意的是，如果我们的文件名中有通配符，如：“*”，那么可以用转义字符“\”，如“\*”来表示真实的“*”字符，而不是任意长度的字符串。 
好吧，还是先来看几个例子吧： 
    clean:
         rm -f *.o
上面这个例子我不不多说了，这是操作系统Shell所支持的通配符。这是在命令中的通配符。 
    print: *.c
         lpr -p ＄?
         touch print
上面这个例子说明了通配符也可以在我们的规则中，目标print依赖于所有的[.c]文件。其中的“＄?”是一个自动化变量，我会在后面给你讲述。 
    objects = *.o
上面这个例子，表示了，通符同样可以用在变量中。并不是说[*.o]会展开，不！objects的值就是“*.o”。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开，也就是让objects的值是所有[.o]的文件名的集合，那么，你可以这样： 
    objects := ＄(wildcard *.o)
这种用法由关键字“wildcard”指出，关于Makefile的关键字，我们将在后面讨论。 
3.4 文件搜寻 
在一些大的工程中，有大量的源文件，我们通常的做法是把这许多的源文件分类，并存放在不同的目录中。所以，当make需要去找寻文件的依赖关系时，你可以在文件前加上路径，但最好的方法是把一个路径告诉make，让make在自动去找。 
Makefile文件中的特殊变量“VPATH”就是完成这个功能的，如果没有指明这个变量，make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量，那么，make就会在当当前目录找不到的情况下，到所指定的目录中去找寻文件了。 
    VPATH = src:../headers
上面的的定义指定两个目录，“src”和“../headers”，make会按照这个顺序进行搜索。目录由“冒号”分隔。（当然，当前目录永远是最高优先搜索的地方） 
另一个设置文件搜索路径的方法是使用make的“vpath”关键字（注意，它是全小写的），这不是变量，这是一个make的关键字，这和上面提到的那个VPATH变量很类似，但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种： 
1.vpath < pattern> < directories> 
为符合模式< pattern>的文件指定搜索目录< directories>。 
2.vpath < pattern>
清除符合模式< pattern>的文件的搜索目录。 
3.vpath 
清除所有已被设置好了的文件搜索目录。 
vapth使用方法中的< pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符，例如，“%.h”表示所有以“.h”结尾的文件。< pattern>指定了要搜索的文件集，而< directories>则指定了的文件集的搜索的目录。例如： 
    vpath %.h ../headers
该语句表示，要求make在“../headers”目录下搜索所有以“.h”结尾的文件。（如果某文件在当前目录没有找到的话） 
我们可以连续地使用vpath语句，以指定不同搜索策略。如果连续的vpath语句中出现了相同的< pattern>，或是被重复了的< pattern>，那么，make会按照vpath语句的先后顺序来执行搜索。如： 
    vpath %.c foo
    vpath %   blish
    vpath %.c bar
其表示“.c”结尾的文件，先在“foo”目录，然后是“blish”，最后是“bar”目录。 
    vpath %.c foo:bar
    vpath %   blish
而上面的语句则表示“.c”结尾的文件，先在“foo”目录，然后是“bar”目录，最后才是“blish”目录。 
3.5 伪目标 
最早先的一个例子中，我们提到过一个“clean”的目标，这是一个“伪目标”， 
    clean:
            rm *.o temp
正像我们前面例子中的“clean”一样，即然我们生成了许多文件编译文件，我们也应该提供一个清除它们的“目标”以备完整地重编译而用。 （以“make clean”来使用该目标） 
因为，我们并不生成“clean”这个文件。“伪目标”并不是一个文件，只是一个标签，由于“伪目标”不是文件，所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然，“伪目标”的取名不能和文件名重名，不然其就失去了“伪目标”的意义了。 
当然，为了避免和文件重名的这种情况，我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”，向make说明，不管是否有这个文件，这个目标就是“伪目标”。 
    .PHONY : clean
只要有这个声明，不管是否有“clean”文件，要运行“clean”这个目标，只有“make clean”这样。于是整个过程可以这样写： 
     .PHONY: clean
    clean:
            rm *.o temp
伪目标一般没有依赖的文件。但是，我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”，只要将其放在第一个。一个示例就是，如果你的Makefile需要一口气生成若干个可执行文件，但你只想简单地敲一个make完事，并且，所有的目标文件都写在一个Makefile中，那么你可以使用“伪目标”这个特性： 
    all : prog1 prog2 prog3
    .PHONY : all

    prog1 : prog1.o utils.o
            cc -o prog1 prog1.o utils.o

    prog2 : prog2.o
            cc -o prog2 prog2.o

    prog3 : prog3.o sort.o utils.o
            cc -o prog3 prog3.o sort.o utils.o
我们知道，Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标，其依赖于其它三个目标。由于伪目标的特性是，总是被执行的，所以其依赖的那三个目标就总是不如“all”这个目标新。所以，其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目标”。 
随便提一句，从上面的例子我们可以看出，目标也可以成为依赖。所以，伪目标同样也可成为依赖。看下面的例子： 
    .PHONY: cleanall cleanobj cleandiff

    cleanall : cleanobj cleandiff
            rm program

    cleanobj :
            rm *.o

    cleandiff :
            rm *.diff
“make clean”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的 
3.6 多目标 
Makefile的规则中的目标可以不止一个，其支持多目标，有可能我们的多个目标同时依赖于一个文件，并且其生成的命令大体类似。于是我们就能把其合并起来。当然，多个目标的生成规则的执行命令是同一个，这可能会可我们带来麻烦，不过好在我们的可以使用一个自动化变量“＄@”（关于自动化变量，将在后面讲述），这个变量表示着目前规则中所有的目标的集合，这样说可能很抽象，还是看一个例子吧。 
    bigoutput littleoutput : text.g
            generate text.g -＄(subst output,,＄@) > ＄@
    上述规则等价于：

    bigoutput : text.g
            generate text.g -big > bigoutput
    littleoutput : text.g
            generate text.g -little > littleoutput
其中，-＄(subst output,,＄@)中的“＄”表示执行一个Makefile的函数，函数名为subst，后面的为参数。关于函数，将在后面讲述。这里的这个函数是截取字符串的意思，“＄@”表示目标的集合，就像一个数组，“＄@”依次取出目标，并执于命令。 
3.7 静态模式 
静态模式可以更加容易地定义多目标的规则，可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法： 
<targets ...>: <target-pattern>: <prereq-patterns ...>
　　　<commands>
...
targets定义了一系列的目标文件，可以有通配符。是目标的一个集合。 
target-parrtern是指明了targets的模式，也就是的目标集模式。 
prereq-parrterns是目标的依赖模式，它对target-parrtern形成的模式再进行一次依赖目标的定义。 
这样描述这三个东西，可能还是没有说清楚，还是举个例子来说明一下吧。如果我们的定义成“%.o”，意思是我们的集合中都是以“.o”结尾的，而如果我们的定义成“%.c”，意思是对所形成的目标集进行二次定义，其计算方法是，取模式中的“%”（也就是去掉了[.o]这个结尾），并为其加上[.c]这个结尾，形成的新集合。 
所以，我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符，如果你的文件名中有“%”那么你可以使用反斜杠“\”进行转义，来标明真实的“%”字符。 
看一个例子： 
    objects = foo.o bar.o

    all: ＄(objects)

    ＄(objects): %.o: %.c
            ＄(CC) -c ＄(CFLAGS) ＄< -o ＄@

上面的例子中，指明了我们的目标从＄object中获取，“%.o”表明要所有以“.o”结尾的目标，也就是“foo.o bar.o”，也就是变量＄object集合的模式，而依赖模式“%.c”则取模式“%.o”的“%”，也就是“foo bar”，并为其加下“.c”的后缀，于是，我们的依赖目标就是“foo.c bar.c”。而命令中的“＄<”和“＄@”则是自动化变量，“＄<”表示所有的依赖目标集（也就是“foo.c bar.c”），“＄@”表示目标集（也褪恰癴oo.o bar.o”）。于是，上面的规则展开后等价于下面的规则： 
    foo.o : foo.c
            ＄(CC) -c ＄(CFLAGS) foo.c -o foo.o
    bar.o : bar.c
            ＄(CC) -c ＄(CFLAGS) bar.c -o bar.o
试想，如果我们的“%.o”有几百个，那种我们只要用这种很简单的“静态模式规则”就可以写完一堆规则，实在是太有效率了。“静态模式规则”的用法很灵活，如果用得好，那会一个很强大的功能。再看一个例子： 

    files = foo.elc bar.o lose.o

    ＄(filter %.o,＄(files)): %.o: %.c
            ＄(CC) -c ＄(CFLAGS) ＄< -o ＄@
    ＄(filter %.elc,＄(files)): %.elc: %.el
            emacs -f batch-byte-compile ＄<
＄(filter %.o,＄(files))表示调用Makefile的filter函数，过滤“＄filter”集，只要其中模式为“%.o”的内容。其的它内容，我就不用多说了吧。这个例字展示了Makefile中更大的弹性。 
3.8 自动生成依赖性 
在Makefile中，我们的依赖关系可能会需要包含一系列的头文件，比如，如果我们的main.c中有一句“#include "defs.h"”，那么我们的依赖关系应该是： 
    main.o : main.c defs.h
但是，如果是一个比较大型的工程，你必需清楚哪些C文件包含了哪些头文件，并且，你在加入或删除头文件时，也需要小心地修改Makefile，这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情，我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项，即自动找寻源文件中包含的头文件，并生成一个依赖关系。例如，如果我们执行下面的命令： 
    cc -M main.c
其输出是： 
    main.o : main.c defs.h
于是由编译器自动生成的依赖关系，这样一来，你就不必再手动书写若干文件的依赖关系，而由编译器自动生成了。需要提醒一句的是，如果你使用GNU的C/C++编译器，你得用“-MM”参数，不然，“-M”参数会把一些标准库的头文件也包含进来。 
gcc -M main.c的输出是： 
    main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h          /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h          /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h          /usr/include/bits/types.h /usr/include/bits/pthreadtypes.h          /usr/include/bits/sched.h /usr/include/libio.h          /usr/include/_G_config.h /usr/include/wchar.h          /usr/include/bits/wchar.h /usr/include/gconv.h          /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h          /usr/include/bits/stdio_lim.h

gcc -MM main.c的输出则是： 
    main.o: main.c defs.h
那么，编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来，我们的Makefile也要根据这些源文件重新生成，让Makefile自已依赖于源文件？这个功能并不现实，不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中，为每一个“name.c”的文件都生成一个“name.d”的Makefile文件，[.d]文件中就存放对应[.c]文件的依赖关系。 
于是，我们可以写出[.c]文件和[.d]文件的依赖关系，并让make自动更新或自成[.d]文件，并把其包含在我们的主Makefile中，这样，我们就可以自动化地生成每个文件的依赖关系了。 
这里，我们给出了一个模式规则来产生[.d]文件： 
    %.d: %.c
            @set -e; rm -f ＄@;              ＄(CC) -M ＄(CPPFLAGS) ＄< > ＄@.＄＄＄＄;              sed 's,\(＄*\)\.o[ :]*,\1.o ＄@ : ,g' < ＄@.＄＄＄＄ > ＄@;              rm -f ＄@.＄＄＄＄
这个规则的意思是，所有的[.d]文件依赖于[.c]文件，“rm -f ＄@”的意思是删除所有的目标，也就是[.d]文件，第二行的意思是，为每个依赖文件“＄<”，也就是[.c]文件生成依赖文件，“＄@”表示模式“%.d”文件，如果有一个C文件是name.c，那么“%”就是“name”，“＄＄＄＄”意为一个随机编号，第二行生成的文件有可能是“name.d.12345”，第三行使用sed命令做了一个替换，关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。 
总而言之，这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖，即把依赖关系： 
    main.o : main.c defs.h
转成： 
    main.o main.d : main.c defs.h
于是，我们的[.d]文件也会自动更新了，并会自动生成了，当然，你还可以在这个[.d]文件中加入的不只是依赖关系，包括生成的命令也可一并加入，让每个[.d]文件都包含一个完赖的规则。一旦我们完成这个工作，接下来，我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令，来引入别的Makefile文件（前面讲过），例如： 
    sources = foo.c bar.c

    include ＄(sources:.c=.d)
上述语句中的“＄(sources:.c=.d)”中的“.c=.d”的意思是做一个替换，把变量＄(sources)所有[.c]的字串都替换成[.d]，关于这个“替换”的内容，在后面我会有更为详细的讲述。当然，你得注意次序，因为include是按次来载入文件，最先载入的[.d]文件中的目标会成为默认目标 
4 Makefile 书写命令 
________________________________________
每条规则中的命令和操作系统Shell的命令行是一致的。make会一按顺序一条一条的执行命令，每条命令的开头必须以[Tab]键开头，除非，命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略，但是如果该空格或空行是以Tab键开头的，那么make会认为其是一个空命令。 
我们在UNIX下可能会使用不同的Shell，但是make的命令默认是被“/bin/sh”——UNIX的标准Shell解释执行的。除非你特别指定一个其它的Shell。Makefile中，“#”是注释符，很像C/C++中的“//”，其后的本行字符都被注释。 
4.1 显示命令 
通常，make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前，那么，这个命令将不被make显示出来，最具代表性的例子是，我们用这个功能来像屏幕显示一些信息。如： 
    @echo 正在编译XXX模块......
当make执行时，会输出“正在编译XXX模块......”字串，但不会输出命令，如果没有“@”，那么，make将输出： 
    echo 正在编译XXX模块......
    正在编译XXX模块......
如果make执行时，带入make参数“-n”或“--just-print”，那么其只是显示命令，但不会执行命令，这个功能很有利于我们调试我们的Makefile，看看我们书写的命令是执行起来是什么样子的或是什么顺序的。 
而make参数“-s”或“--slient”则是全面禁止命令的显示。 
4.2 命令执行 
当依赖目标新于目标时，也就是当规则的目标需要被更新时，make会一条一条的执行其后的命令。需要注意的是，如果你要让上一条命令的结果应用在下一条命令时，你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令，你希望第二条命令得在cd之后的基础上运行，那么你就不能把这两条命令写在两行上，而应该把这两条命令写在一行上，用分号分隔。如： 
    示例一：
        exec:
                cd /home/hchen
                pwd

    示例二：
        exec:
                cd /home/hchen; pwd
当我们执行“make exec”时，第一个例子中的cd没有作用，pwd会打印出当前的Makefile目录，而第二个例子中，cd就起作用了，pwd会打印出“/home/hchen”。 
make一般是使用环境变量SHELL中所定义的系统Shell来执行命令，默认情况下使用UNIX的标准Shell——/bin/sh来执行命令。但在MS-DOS下有点特殊，因为MS-DOS下没有SHELL环境变量，当然你也可以指定。如果你指定了UNIX风格的目录形式，首先，make会在SHELL所指定的路径中找寻命令解释器，如果找不到，其会在当前盘符中的当前目录中寻找，如果再找不到，其会在PATH环境变量中所定义的所有路径中寻找。MS-DOS中，如果你定义的命令解释器没有找到，其会给你的命令解释器加上诸如“.exe”、“.com”、“.bat”、“.sh”等后缀。 
4.3 命令出错 
每当命令运行完后，make会检测每个命令的返回码，如果命令返回成功，那么make会执行下一条命令，当规则中所有的命令成功返回后，这个规则就算是成功完成了。如果一个规则中的某个命令出错了（命令退出码非零），那么make就会终止执行当前规则，这将有可能终止所有规则的执行。 
有些时候，命令的出错并不表示就是错误的。例如mkdir命令，我们一定需要建立一个目录，如果目录不存在，那么mkdir就成功执行，万事大吉，如果目录存在，那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录，于是我们就不希望mkdir出错而终止规则的运行。 
为了做到这一点，忽略命令的出错，我们可以在Makefile的命令行前加一个减号“-”（在Tab键之后），标记为不管命令出不出错都认为是成功的。如： 
   clean:
            -rm -f *.o
还有一个全局的办法是，给make加上“-i”或是“--ignore-errors”参数，那么，Makefile中所有命令都会忽略错误。而如果一个规则是以“.IGNORE”作为目标的，那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法，你可以根据你的不同喜欢设置。 
还有一个要提一下的make的参数的是“-k”或是“--keep-going”，这个参数的意思是，如果某规则中的命令出错了，那么就终目该规则的执行，但继续执行其它规则。 
4.4 嵌套执行make 
在一些大的工程中，我们会把我们不同模块或是不同功能的源文件放在不同的目录中，我们可以在每个目录中都书写一个该目录的Makefile，这有利于让我们的Makefile变得更加地简洁，而不至于把所有的东西全部写在一个Makefile中，这样会很难维护我们的Makefile，这个技术对于我们模块编译和分段编译有着非常大的好处。 
例如，我们有一个子目录叫subdir，这个目录下有个Makefile文件，来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写： 
    subsystem:
            cd subdir && ＄(MAKE)

其等价于：

    subsystem:
            ＄(MAKE) -C subdir
定义＄(MAKE)宏变量的意思是，也许我们的make需要一些参数，所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录，然后执行make命令。

我们把这个Makefile叫做“总控Makefile”，总控Makefile的变量可以传递到下级的Makefile中（如果你显示的声明），但是不会覆盖下层的Makefile中所定义的变量，除非指定了“-e”参数。

如果你要传递变量到下级Makefile中，那么你可以使用这样的声明：
export <variable ...>
如果你不想让某些变量传递到下级Makefile中，那么你可以这样声明： 
unexport <variable ...>
如： 
    
    示例一：

        export variable = value

        其等价于：

        variable = value
        export variable

        其等价于：

        export variable := value

        其等价于：

        variable := value
        export variable

    示例二：

        export variable += value

        其等价于：

        variable += value
        export variable
如果你要传递所有的变量，那么，只要一个export就行了。后面什么也不用跟，表示传递所有的变量。 
需要注意的是，有两个变量，一个是SHELL，一个是MAKEFLAGS，这两个变量不管你是否export，其总是要传递到下层Makefile中，特别是MAKEFILES变量，其中包含了make的参数信息，如果我们执行“总控Makefile”时有make参数或是在上层Makefile中定义了这个变量，那么MAKEFILES变量将会是这些参数，并会传递到下层Makefile中，这是一个系统级的环境变量。 
但是make命令中的有几个参数并不往下传递，它们是“-C”,“-f”,“-h”“-o”和“-W”（有关Makefile参数的细节将在后面说明），如果你不想往下层传递参数，那么，你可以这样来： 
    
    subsystem:
            cd subdir && ＄(MAKE) MAKEFLAGS=
如果你定义了环境变量MAKEFLAGS，那么你得确信其中的选项是大家都会用到的，如果其中有“-t”,“-n”,和“-q”参数，那么将会有让你意想不到的结果，或许会让你异常地恐慌。 
还有一个在“嵌套执行”中比较有用的参数，“-w”或是“--print-directory”会在make的过程中输出一些信息，让你看到目前的工作目录。比如，如果我们的下级make目录是“/home/hchen/gnu/make”，如果我们使用“make -w”来执行，那么当进入该目录时，我们会看到： 
    
    make: Entering directory `/home/hchen/gnu/make'.
而在完成下层make后离开目录时，我们会看到： 
    
    make: Leaving directory `/home/hchen/gnu/make'
当你使用“-C”参数来指定make下层Makefile时，“-w”会被自动打开的。如果参数中有“-s”（“--slient”）或是“--no-print-directory”，那么，“-w”总是失效的。 
4.5 定义命令包 
如果Makefile中出现一些相同命令序列，那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始，以“endef”结束，如： 
    define run-yacc
    yacc ＄(firstword ＄^)
    mv y.tab.c ＄@
    endef
这里，“run-yacc”是这个命令包的名字，其不要和Makefile中的变量重名。在“define”和“endef”中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序，因为Yacc程序总是生成“y.tab.c”的文件，所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。 
    foo.c : foo.y
            ＄(run-yacc)
我们可以看见，要使用这个命令包，我们就好像使用变量一样。在这个命令包的使用中，命令包“run-yacc”中的“＄^”就是“foo.y”，“＄@”就是“foo.c”（有关这种以“＄”开头的特殊变量，我们会在后面介绍），make在执行命令包时，命令包中的每个命令会被依次独立执行。
]]></description>
<pubDate>
2006-04-18 12:03:06.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308044.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308044.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[全世界最有名十大宝藏完全探秘]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308136.shtml</link>
<description>
<![CDATA[人类发展的历史同时也是积累财富的历史。生产力的提高、社会形式的变迁、人们对精神生活的追求和思想境界的不断升华无一不和财富的积累有关系。人类不断创造着财富，财富同时又推动人类前进。 

　　不过，在历史的过往里，也有一部分财富因为各种各样的原因停驻在时间隧道里，它们或者被深埋在地下，或者被故意隐藏，或者被秘密收藏，成为富有神秘色彩的宝藏。 

　　意外发现宝藏大概是所有人的梦想，无数文艺作品以寻宝为题材而引人入胜。下面的文章将介绍世界十大宝藏，它们有的是一个国家或者一个家族千年的积累，有的是一个人经过一生探寻得到的回报，有的是考古学家意外的发现。每个宝藏都是个极富传奇色彩的故事。 

　　第一大宝藏：图特卡蒙陵墓 

　　埃及的帝王谷位于尼罗河西岸的沙漠中，古埃及新时期(首都设在底比斯以后)的大多数法老都埋葬在这里。在1900年左右，几乎所有帝王谷里的陵墓都被发现了，考古学家和盗墓者在这方面平分秋色。但是仍然有成群的人在帝王谷里转悠，他们都是在寻找传说中国王图特卡蒙的陵墓。 

　　图特卡蒙是3300多年前的一个年轻埃及法老，他曾在金雕御座上管理着庞大帝国。他的统治是短暂的，在18岁时突然死去。在埃及漫长的法老时代中，图特卡蒙因为在位时间短而名不见经传，他的猝死也使得他没有事先修建的豪华金字塔陵墓。正因为不起眼，其陵墓在很长时间里始终没有被发现。 

　　考古学家霍华德·卡特熟读古埃及历史，发现图特卡蒙陵墓是他毕生的梦想。1903年起，他就带领助手在帝王谷的每一寸土地上搜索，1922年11月5日，在19年的努力后，他终于找到了图特卡蒙陵墓入口。它竟然位于另一个著名的法老拉美西斯六世的陵墓下面，开凿于岩石内。 

　　这是3300年来惟一一个完好无缺的法老陵墓，也是埃及最豪华的陵寝，更是埃及考古史乃至世界考古史上最伟大的发现。卡特之前以为这个年轻法老的墓葬品会比较简单，谁知之后长达3年时间的挖掘向全世界证实了这种预想的愚蠢。卡特说过，图特卡蒙一生惟一出色的成绩就是他死了并且被埋葬了，这话是有道理的。因为其陵墓的发现成为古代文明对现代人类最彻底的一次震撼和嘲笑。那个成为埃及文明象征的纯金面具，那个纯金制成的棺材，那个由纯金雕制镶满宝石的王位，那些铺满墓室墙壁的纯金浮雕，那具完整无缺的木乃伊......所有一切都让人类惊叹，3300年前埃及人的工艺技巧和现在的我们到底有什么不同？ 

　　图特卡蒙陵墓的发现是世界考古工作成功的顶峰，也是考古史的重要转折点。所有出土文物超过10000件，每件都是无价之宝。卡特花费3年的时间把它们全部运出墓室，当时挖掘人员从墓的出口抬出女神哈托尔牛头灵床的镜头已经成为考古史上无法超越的经典；埃及政府又花费了整整10年的时间把它们运到开罗，开罗博物馆之前的所有藏品都因之黯然失色；而彻底研究它们可能需要未来人类全部的时间。 

　　文物无可比拟的历史价值和所蕴涵的谜团使图特卡蒙陵墓排在世界十大宝藏的第一位。

　　二、英国王室珠宝 

　　大约在1600年前，地球上崛起了一个强大的王族，这就是英国王室。英国王室是现存最古老的王族，而每代君主的加冕仪式都严格奉行完全一样的传统，这使得英国王室的加冕典礼成为现存的、依然举行的最古老的仪式。在加冕仪式上，国王或者女王头戴的王冠和手持的权仗都成为全球瞩目的焦点。 

　　为了使王冠和权仗成为世界上独一无二的权力象征，历代王室想尽办法收集钻石和珠宝，认为稀世的钻石最能体现王室尊贵。长达几个世纪收集钻石的历程逐渐形成世界上最有名的家族珍宝。早期那些伟大英王和王后佩带过的王冠已经找不到了。国王及其亲属为了发动战争、重建毁于大火的王宫和举办王室婚礼，不得不卖掉了许多珍宝。在中世纪，国王通常在作战时带上御宝，因为他们不信任留在宫中的皇亲国戚。1648年英国爆发的反王权运动对英国王室冲击极大，很多珍贵王冠和权仗流失了。1660年英王室复辟以后，开始大规模的重新制作王冠和权仗的工程，从那时到现在，很多稀世珍品都被保存了下来。随着王室的发展，从18世纪开始，英王室有了专用的珠宝工匠，他们用非凡的技艺制作出最精美的首饰。 

　　随着势力的不断扩张，英国成为世界最强大的殖民帝国。其中殖民地印度和南非都以出产钻石以及珍稀宝石闻名，这两地向英王室供应了无数一流钻石。而一些弱小的国家也愿意把本国最珍贵的珠宝献给英国，大多怀着破财免灾的想法。 

　　王室成员都根深蒂固地习惯于把珠宝换来换去。本以为镶嵌在爱德华国王十一世入棺时所戴戒指上的一枚蓝宝石，如今却闪耀在"帝国之冠"上，这顶王冠上还镶有两串珍珠，据报道，那正是苏格兰女王玛丽1587年被斩首时戴的项链。19世纪的君主维多利亚女王尤其热衷于收藏珠宝，从帝国各地搜罗来的奇珍异宝令她陶醉不已。她的珍品中包括一枚拇指大小的印度钻石，名叫"光明之山"，是现今发现的最古老的钻石，于1304年发现于印度，原重191克拉，后来维多利亚女王嫌它光泽度不好要再加工，它被磨得只剩108.93克拉。正是这枚被镶嵌在女王王冠上的钻石激发了威尔基·科林斯的灵感，写出《月亮宝石》这部经典作品。 

　　然而，在有史以来最大的钻石"非洲之星"面前，"光明之山"也相形见绌。1905年南非发现了重达3106克拉的钻石原矿，新开通的跨大西洋电缆将消息迅速传遍全球，当时宝石界行家就估计原矿的价值高达75亿美元。由于南非当时是英国的殖民地，大家一致认为应把它运往伦敦，献给爱德华七世国王。这件举世无双的珍品引起世界各地珠宝大盗想入非非，有关人员花了几个月时间考虑如何保障运输安全。最后，伦敦警察厅决定，最佳原则是"越简单越安全"。大如茄子的钻石被装进一个没有任何标识的包裹邮寄出去，一个月后出现在白金汉宫的皇家邮袋里。1908年2月10日，这颗巨钻被劈成几大块后加工。加工出来的成品钻总量为1063.65克拉，全部归英王室所有。最大的一颗钻石取名为"库里南1号"，也被称做"非洲之星"，重530.02克拉。第二大的被命名为"库里南2号"，重317.4克拉。现在鸡蛋大小的"非洲之星"被镶嵌在英王的权仗顶端，权仗上还有2444颗钻石。鸽子蛋大小的"库里南2号"被镶嵌在英王室最重要的王冠"帝国王冠"上。 

　　人类开采利用钻石的历史已近几千年，但大于20克拉的钻石就极为罕见，而大于100克拉的钻石更被视为国宝。但是这样国宝级的钻石在英王室的收藏中就有好几颗。 

　　现在王室已不再盲目追求将最大的钻石全部集中在王冠上。要知道威廉四世国王1830年加冕时就闹出笑话而未能尽兴。这位喜爱奢华的君主坚持把所有钻石和宝石镶嵌到王冠上。结果王冠太沉，国王的脖子一阵剧痛，不得不中断加冕典礼，随后拔掉一颗臼齿。 

　　英王室拥有22599件宝石和宝器，但实际价值难以统计。 

　　英王室珠宝因其代表着最古老王室的尊贵和传统排在世界十大宝藏的第二位。

　　三、阿托卡夫人号沉船 

　　西班牙对殖民财富的掠夺采用了最野蛮的方式，当时南美洲被证实富含金银矿和其他稀有资源，于是西班牙殖民者在新大陆惟一的工作就是开采和经营矿山。一船又一船的金银财宝成为殖民掠夺的罪证。 

　　西班牙的运金船最害怕海盗和飓风，为了对付海盗，每支船队都配备有装备了大炮、船身坚固的"护卫船"，阿托卡夫人号就是这样一艘护卫船。1622年8月，阿托卡夫人号所在的，由29艘船组成的船队载满财宝从南美返回西班牙。由于是护卫船，大家把最贵重、最多的财宝放在阿托卡夫人号上，遗憾的是阿托卡夫人号的大炮对飓风没有什么威慑力。当船队航行到哈瓦那和古巴之间海域时，飓风席卷了船队中落在最后的5艘船。阿托卡夫人号由于载重太大，航行速度最慢，成为首当其冲的袭击目标。船很快沉到深17米的海底。其他船只上的水手马上跳下水，希望抢救出一些财宝，但是就在他们找到残骸，准备打捞金条时，又一场更具威力的飓风袭来，所有水下的人都在飓风中丧生。 

　　梅尔·费雪给自己的定义是寻宝人。1955年他成立了一个名叫"拯救财宝"的公司，专门在南加州一带的海域寻找西班牙沉船。20年的打捞生涯里，费雪先后打捞起6条赫赫有名的西班牙沉船，成为圈中名人，也赚了大把钞票。不知不觉，费雪到了该退休的年龄，不过他不愿意离开打捞船，因为他曾发誓一定要找到传说中有着最多财宝的阿托卡夫人号。于是全家人为这个理想放弃了公司的正常运转，费雪的妻子、儿子和女儿陪着父亲一起下水，在海底寻找梦想。他们的搜寻一丝不苟，只要看到不是石头的东西都要用金属探测器探测。1985年7月20日，费雪和家人找到了阿托卡夫人号和上面数以吨计的黄金，不过这种喜悦却被30年的艰难磨得平淡。费雪认为上帝一定会让他找到阿托卡夫人号，只不过一直考验他的耐心而已。 

　　这个号称海底最大宝藏的沉船上有40吨财宝，其中黄金就有将近8吨，宝石也有500公斤，所有财宝的价值约为4亿美元。费雪寻找阿托卡夫人号的故事在美国成了中国"铁杵磨成针"的故事，"寻找阿托卡"竟然也成了常用短语，意思是坚持梦想，必会成功。 

　　阿托卡夫人号上的宝藏完全是以量取胜，以吨计的黄金和一个家庭30年的流年使它排在世界十大宝藏的第三位。 

　　四、赫氏堡 

　　赫氏堡是有史以来最豪华的私人住宅，是上个世纪20年代美国传媒巨人威廉·伦道夫·赫斯特的私人城堡。 

　　在赫斯特事业的巅峰时期，他拥有两座矿山，数不清的地产，26家报纸，13家全国性刊物，8家广播电台和许多其他新闻媒体事业。当时赫斯特每天能赚5万美元，这个数字相当于现在的500万美元。 

　　每个成功人士都想修建一座梦想中的住宅，赫斯特也一样，1919年他开始构思修建一座举世无双的私人城堡。赫氏堡建在距洛杉矶360公里的圣西蒙。这里从太平洋边开始到桑塔露西亚山，4万英亩的土地都是赫家的私产。那广袤的草场，绵延的山丘，举目可望的海景，在赫斯特心中有不可替代的位置。到1919年，当他能够实现城堡之梦时，他毫不犹豫地选择了此地作为基址。 

　　赫氏堡是由朱莉亚·摩根设计的，她是世界上最早从事建筑设计的女性之一。不过精通艺术的赫斯特在施工的同时给予摩根很多建议，其中大部分是关于如何将几千件古董收藏填进房间而又不显突兀，好像那些古董几百年来一直在那里一样。 

　　1925年的圣诞节，尽管还有一些房间没能完工，赫斯特一家正式搬进了城堡。随后著名的艺术家、文学家、好莱坞明星、政客、将军们纷纷被邀请到赫氏堡做客，当音乐家萧伯纳参观完赫氏堡以后感慨地说："如果上帝有钱，他大约也会为自己修建这样的住所。" 

　　赫氏堡的豪华超越所有人的想像，因为其中的艺术珍品是无价的。赫斯特一生酷爱收藏艺术品，家具、挂毯、绘画、雕塑、壁炉、天花板、楼梯，甚至整个房间都是他的收藏对象。他的收藏大多布置在城堡的房间内供人欣赏和使用，丝毫没有将藏品作为投资以期升值等功利思想。因为有了这些艺术品，整个城堡平添了浓浓的艺术气息和典雅的风韵。 

　　赫氏堡的主楼共有115个房间，计有卧室42间，起居室19间，浴室61间，2个图书室，1个厨房，1个弹子房，1个电影厅，1个聚会厅，1间大餐厅。此外还有3栋独立的客房，整个山庄共有房间165间。 

　　位于城堡主入口处的室外游泳池叫海王池。按照萧伯纳的逻辑，海王爷本人游泳的地方一定比这儿差远了。泳池长32米，深1米到3米，所蓄的1300吨水是从山上引来的泉水。池边散落着几尊希腊罗马神话传说中的人物雕像，全部是艺术珍品。室内游泳池叫罗马池，是世界最豪华的泳池。墙壁、池底、岸边、跳台等用了1500万块在威尼斯制造的玻璃马赛克拼贴表面。金色的玻璃马赛克表面贴的是一层真金。单是生产这些马赛克就花了一年3个月的时间，整个泳池的修建则历时3年。 

　　城堡中的大图书室是专为客人们布置的。那里收藏的手稿、绝版书、善本书全部是世所罕见。书柜顶和书桌上放置的是公元前2世纪到8世纪希腊的陶罐，书桌和扶手椅是核桃木的古董。曾经让来做客的邱吉尔声称自己可以足不出户在该图书室待好几个月。 

　　整座城堡只有一个餐厅，餐厅内的布置是赫斯特的骄傲。进入餐厅你会以为自己到了天主教堂或修道院。餐厅墙上挂的是16世纪法国佛兰德壁毯，椅子是14世纪西班牙唱诗班的长椅，天花板是17世纪意大利的木制天花板，上面雕刻的圣徒像比真人还大。房间尽头的大壁炉可以容下三四个人而丝毫不用弯腰低头，也不拥挤。壁炉上挂的一排旗帜是16世纪意大利锡耶那城举行宗教赛马活动时胜利者的旗子。桌上银制的餐具和烛台是17到19世纪英国、西班牙、法国等地的精品。赫斯特热爱动物，赫氏堡所在的牧场上建有一个动物园，是全球最大的私人动物园。赫斯特也热爱自然，修建赫氏堡时，有许多大橡树挡住了路，赫斯特宁肯花几千美元将树移走，也不愿简单地将它们伐掉。 

　　光是修建赫氏堡的花费就高达1000万美元，这在当时相当于一个国王的身家。如果计算上所有古董和艺术品的价值，谁也说不清赫氏堡到底值多少钱。赫斯特去世后他的儿子们决定将城堡捐赠给加州政府，使整个产业得以向公众开放，让世人共同领略迷人山庄的魅力。 

　　赫氏堡以其无价的艺术珍宝和主人的慷慨排在世界十大宝藏的第四位。 

　　五、罗亚尔港 

　　16世纪，中、南美洲是西班牙的天下，殖民强盗搜刮了大量金银财宝，一船船运回欧洲。在入侵西半球方面，英国落后西班牙一步，除了控制北美洲北部地区以外，很难染指西班牙的势力范围。心理不平衡的英国嫉妒西班牙抢到的巨额财富，就怂恿海盗专门袭击西班牙的船只，并为之提供庇护所。与此同时，欧洲一些亡命之徒沦为海盗，在美洲沿海抢劫过往商船，特别对抢劫西班牙皇家的运金船更感兴趣。英国政府当时专门辟出英属殖民地牙买加岛东南岸的罗亚尔港作为海盗的基地，罗亚尔港于是成为历史上海盗船队的最大集中地。 

　　罗亚尔港公开身份是牙买加首府，非正式身份是海盗首都，海盗抢夺来的金银珠宝在这里堆成山，一船船金子有的时候都轮不到卸船，只有停放在港口里等候。这里是人类历史上最邪恶的城市，也是最堕落的城市，虽然只有几万人生活在这里(其中大约6500人是海盗)，但城市的奢侈程度远远超越当时的伦敦和巴黎。整个城市没有任何工业，却可以享受最豪华的物质生活。中国的丝绸、印尼的香料、英国的工业品一应俱全。当然最多的还是金条、银条和珠宝。 

　　1692年6月7日，罗亚尔港仍像往常一样热闹，酒馆人声嘈杂，销赃市场顾客如云，各式船只频繁进出港口，满载着工业品的英国船在码头卸货，美洲大陆的过境船在修帆加水。海盗船混迹其间，一般人难以辨别出来。但是这个罪恶之城注定要受到上帝的惩罚。 

　　中午时分，忽然大地颤动了一下，接着是一阵紧过一阵的摇晃。地面出现巨大裂缝，建筑物纷纷倒塌。土地像波浪一样在起伏，地面同时出现几百条裂缝，忽开忽合。海水像开了锅，激浪将港内船只悉数打碎。穿金戴银的人在屋塌、地裂、海啸的交逼下疯狂奔走，企图找一个庇身之所。11时47分，一阵最猛烈的震动后，全城2/3没于海水底下，残存陆地上的建筑物也被海浪冲得无影无踪。 

　　罗亚尔港从此消失在大海中，直到1835年，在风平浪静的日子里，人们仍能清楚地看见海底城市的痕迹———一些沉船、房屋依稀可辨。当时测量，沉城处于海平面之下7到11米。再以后泥沙和垃圾层层覆盖，罗亚尔港在人们的记忆中湮灭了。 

　　牙买加独立以后，政府一直没有放弃寻找这个海葬城市。1959年，牙买加政府和海下考古学家罗伯特·马克思签订挖掘条约。条约规定马克思只负责挖掘，而挖出的所有财宝都归牙买加政府所有。在之后的时间里，马克思找到了一部分城市遗址，并挖出了价值几百万美元的珠宝和大批生活用品。其中最有历史价值的是一只怀表，表针指向11时47分，由此确认了古城沉没的时间。而最有趣的是一尊没有头的雕像，专家研究证实这是中国人信奉的观音。4年以后，马克思以"再也挖不到财宝"为由离开牙买加。所有的人都不相信罗亚尔港只有这一点财宝，但谁也猜不出马克思离去的真实原因。 

　　1990年，美国得克萨斯州A＆M大学接到牙买加政府的邀请，再次开始罗亚尔港的挖掘工作。A＆M大学的专家们准确找到罗亚尔港的主要沉没地点，他们发现当年马克思挖出来的宝藏只是非常小的一部分，99%的宝藏还沉在海水里。现在罗亚尔港宝藏的寻找工作还在继续，不过牙买加政府没有决定打捞已经发现的物品和金银。没有人知道这个被海葬的海盗首都到底还能给人类带来多少惊喜。 

　　罗亚尔港罪恶的兴起和被自然覆灭的悲剧结果使它排在世界十大宝藏的第五位。 

　　六、丹漠洞遗址宝藏 

　　爱尔兰的基尔肯尼郡是一个风光旖旎的地方，也是爱尔兰最重要的旅游城市之一。每年都有数十万计的游客来到基尔肯尼，他们必定参观的地方是丹漠洞遗址。 

　　丹漠洞被称为爱尔兰最黑暗的地方，因为这个洞穴记录了一次惨无人道的大屠杀。公元928年，挪威海盗来到爱尔兰，对基尔肯尼附近一带进行洗劫。当时居住在丹漠洞附近的居民为了逃命，在海盗袭来的前几个小时集体躲到洞中。丹漠洞是一个巨大的溶洞，洞里地形复杂，有连串的小洞穴一一相连，避难的人认为这是绝佳的藏身之地。他们幻想海盗抢完能抢的东西后就会离开。然而丹漠洞的入口太过明显，海盗很快发现了洞中藏人的秘密，一场血腥的大屠杀开始了。海盗进入洞里，把所有发现的人都杀死，估计有1000多人，然后守在洞口半个月，没有当场被杀死的人后来都因感染而死或者饿死了。 

　　在之后将近1000年的时间里，丹漠洞成了爱尔兰的"地狱入口"，再没有一个人敢进入洞中。直到1940年，一群考古学家对丹漠洞进行考察，仅仅在一个小洞穴里就发现44具骸骨，多半是妇女和老人的，甚至还有未出世的胎儿的骨骼。骸骨证实了丹漠洞曾经的悲剧，1973年这里被定为爱尔兰国家博物馆，每年迎接无数游客前来纪念那些惨遭屠杀的人。 

　　然而，丹漠洞的故事到这里还没有结束。1999年，一个导游的偶然发现证实，这里不仅是黑暗历史的纪念馆，沉默的洞穴中还隐藏了永恒的宝藏。 

　　1999年冬天，一个导游准备打扫卫生，因为寒冷冬季是旅游淡季，丹漠洞将关闭一段时间。他准备仔细清理游客留下的垃圾，所以去了很多平时根本不会去的洞穴。在一个离主路很远的小洞里，导游突然看到一块绿色的"纸片"粘在洞壁上，他以为那是一张废纸。走上前去，赫然发现那根本不是什么纸片，而是什么东西从洞壁的狭缝中发出闪闪绿光。导游用手指往外抠，结果抠出一个镶嵌着绿宝石的银镯子！ 

　　诚实的导游马上将发现报告政府，在接下来的3个月里，爱尔兰国家博物馆的工作人员从那个狭缝中挖出了几千枚古钱币，一些银条、金条和首饰，另外还有几百枚银制纽扣。这些东西应该是当时躲藏的人随身携带的。也许为了让财物更安全，他们把值钱的东西集中然后藏在一个隐蔽小洞里，甚至把衣服上的银纽扣都解了下来。海盗之所以屠杀所有的人，也许和没能发现这些财宝有关。由于在潮湿的洞里呆了1000多年，挖出来的东西都失去了金属原有的夺目光彩。国家博物馆的几十个专家工作了几个月才让所有艺术品和钱币重现光彩。 

　　丹漠洞遗址宝藏是爱尔兰最重要的宝藏，被收藏在国家博物馆，一直没有完全对外展示过。虽然宝物数量不是最多，但其历史价值和考古价值远远超过其本身价值。考古人员说，有一些工艺品和纽扣的样式十分古怪，在所有和海盗有关的文物中都是独一无二的。在丹漠洞中被杀害的人现在可以安息了，他们为之丧命的财宝现在成了爱尔兰的国宝，将永远聆听世人的惊叹和赞美。 

　　丹漠洞遗址宝藏因为其独一无二的血腥背景和考古价值排在世界十大宝藏的第六位。 

　　七、陨石收藏 

　　天上掉馅饼是不太可能的，但是从天上掉下和黄金一样值钱的东西却有可能，那就是很多人都想不到的陨石。 

　　罗伯特·黑格今年45岁，他有双重身份，一方面是加州大学洛杉矶分校的教授，另一方面是全球最权威的陨石收藏家。从23岁起，黑格就开始收集陨石，当时还没有人意识到那是可以卖钱的好东西。到现在为止，黑格拥有的陨石成为世界上最大的私人陨石收藏。虽然自1990年起，也有其他人进入这个行当，但从实力和收藏规模来说还没有人能和黑格相比。最初黑格收集陨石只是出于兴趣，但后来他发现陨石因为稀有而珍贵，也可以卖好价钱。现在在专业的陨石市场上，贵的价格为每克超过8美元，几乎和黄金价格一样。就算最一般的每公斤也在30美元左右。如果是含有稀有金属的陨石，那么价格就难以计量了。 

　　黑格收集陨石的经历很像电影《夺宝奇兵》的情节，充满惊险、刺激和传奇色彩。为了寻找从天而降的财富，他的足迹遍及地球上除南极以外的所有大陆。在智利、纳米比亚、澳大利亚、墨西哥和埃及，他都有在旷野中九死一生的经历。只要美国航天航空局预报什么地方什么时候将会有流星雨，他都会在准确的时间赶到那里。无论在什么地方，无论搭乘什么交通工具。除了自己寻找陨石，他还向当地人收购，当地人只要找到陨石，不论大小，黑格都会用现金收购。1992年，黑格在阿根廷以重金收购了一块重达37吨的陨石，那是他一生中看到的最大的陨石。但是在把陨石运出海关时，阿根廷政府以走私罪罪名将他逮捕，认为这块罕见的陨石归阿根廷国家所有。后来黑格被释放了，但陨石就被永远留在了阿根廷。 

　　没有流星雨的时候，黑格也会自己搜寻陨石。他主要在非洲的沙漠地带搜寻，因为那里的陨石从来没被人捡走。黑格驾驶着滑翔降落软翼机在沙漠上方120米的高处慢慢飞翔，只要看到有突出物就降落，然后用金属探测器搜索。一般人认为这样无异于大海捞针，不过黑格20多年来在沙漠中发现的陨石占他私人收藏的相当一部分。 

　　目前，黑格的陨石收藏按市场价计算已经超过3000万美元，随着越来越多人开始收集陨石，他的收藏只会成倍地增值。 

　　由于其来源的特殊性和未来增值的潜力，黑格的陨石收藏排在世界十大宝藏的第七位。 

　　八、西潘王墓室 

　　秘鲁是南美文明古国，境内古文化遗址密布。在秘鲁发现的伟大遗迹有很多，比如说马丘比丘。但是绝大多数遗址都没有宝藏遗留。一方面是因为当时的殖民宗主国西班牙在秘鲁境内翻得底朝天，大部分财宝都被掠夺走了。另一方面，秘鲁民间盗窃文物的现象极为猖獗，当地人只要发现文物马上就一哄而上，一抢而光。 

　　西潘王墓室其实就是被盗墓者发现的。1987年前后，国际文物黑市上频频出现显然是来自秘鲁，但是绝对不属于印加文明的文物。敏感的考古学家阿尔瓦博士意识到这些独特的文物表明很可能又有一个重要遗迹被盗了。他和助手火速赶到秘鲁北部奇科拉约附近，一边询问一边搜寻，终于在1988年发现了西潘王墓室。西潘王墓隐藏在一个山谷里，位置很隐秘，周围没有任何显著标志，几乎可以说是很卑微，这成为它一直没有被打扰的原因。墓的入口已经被盗墓者打开，整个墓由大小几十个墓室组成，豪华的墓室和丰富的陪葬品让阿尔瓦博士目瞪口呆。 

　　为了继续保护文物不被盗窃，阿尔瓦博士固执地坚持住在墓里，守住入口直到秘鲁国家文物局的官员到达。当地的农民憎恨阿尔瓦断了他们的财路，在洞口威胁说要把他杀死。幸运的是，文物最终被保护。在之后的挖掘工作中，阿尔瓦博士挖到了密封的、从未被进入的西潘王主墓室，他因此也成为世界考古史上的明星。 

　　西潘王是古代莫切人的一位帝王。莫切人生活在公元100年到700年之间，后来被印加人征服。一直以来印加文明是秘鲁古代文明的中心，很难想像在莫切人的古迹中却发现了令印加文物都黯然失色的宝贝。 

　　西潘王的墓室里摆满了琳琅满目的陪葬品，西潘王的尸骨放在墓室的最中间，他的手中抓着一个重达0.5公斤、纯金制成的小铲子。他的头上和前胸覆盖着华丽的金制面具，他手臂的骨骼上挂满精美的首饰，就连他的尸体周围都堆满了的数不清的首饰和工艺品。西潘王似乎想把生前收集到的所有财富都带到来生的世界里去。这些还不算全部，最夸张的是，西潘王的四周有几十具陪葬者的尸体，他们中有年轻的女人、侍卫、仆人，而这些人的尸体上无一不是堆满了金银制成的首饰。整个墓穴中，死者的骸骨只是点缀在一堆金银珠宝中的星星白色。阿尔瓦博士说，之前在文物黑市上看到的东西简直没法和西潘王墓室中的发现相比，如果盗墓者先发现主墓室，那么后果不堪设想。 

　　西潘王墓室的发现是整个西半球最辉煌的墓葬文物发现，被喻为新大陆的"图特卡蒙墓"。现在所有的金银首饰和工艺品都被当地博物馆保管。 

　　西潘王墓室以巨额的金银陪葬品和千钧一发的危险命运排在世界十大宝藏的第八位。

　　九、霍克森钱币 

　　英格兰萨福克郡有一个名叫霍克森的小村庄。村子里的人都靠务农为生，他们的生活宁静且平淡。但是1992年11月16日，这种宁静和平淡被打破。霍克森历史上最重要的一天到来了，这个村庄因为一份宝藏被意外发现而名噪全球。 

　　艾瑞克·劳斯是霍克森的一个普通农民。1992年的11月，他打算把自己的住宅改装，为此好朋友和邻居都前来帮忙。11月15日，屋子的装修工程结束了，但一个朋友却告知劳斯自己的锤子不见了。劳斯从不愿占别人便宜，因此在院子里整整找了一天，但一无所获。他猜想锤子可能被埋到了地下，于是16日一早，他买了一个金属探测器，继续在院子里寻找。 

　　到了中午，金属探测器突然发出警报声，劳斯以为发现锤子了，开始在院子里挖起来，可挖到50厘米深的地方时还没有东西。劳斯并没有打算放弃，随着坑越挖越深，探测器发出的声音亦越来越大。在挖到差不多1.5米深的地方时，一枚银币突然跳了出来。仔细一看，这是一枚古罗马时代的银币，虽然金属已经严重变色，但古罗马帝王头像的浮雕还清晰可见。劳斯继续挖掘，接下来的情景让他一辈子都忘不掉————呈现在他眼前的是一堆古罗马银币，中间夹杂着不少闪闪发光的金币，偶尔还有银制的汤匙和小艺术品，他挖到了一个地下宝藏。 

　　劳斯马上停止挖掘，并向萨福克郡文物管理委员会报告了发现。文物管理委员会的成员以最快速度赶到劳斯家。经过专业人士一天的挖掘，所有宝物都重见天日。其中有14191枚银币、565枚金币、24枚铜币、一些工艺品、首饰和金块。 

　　所有金币都是纯度超过99%的九分七币(一种古罗马金币的专称)，在公元394年到公元405年之间铸造。全部金币来自13个不同的造币厂，从出厂到埋入地下都只有不到50年的流通时间，所以保存得格外完好。在一般文物市场上，这种金币是很罕见的，就算有，价格也高得吓人。而一下子发现565枚这样的金币在历史上还是第一次。 

　　除了古罗马钱币以外，霍克森宝藏里还有超过79个银汤匙，20多个银烛台，一些银制的小雕像和29件纯金制成的、做工精细的首饰。 

　　这些首饰上镶嵌的宝石在被埋藏之前都已经被撬下来，或许宝藏的主人觉得宝石价值高而且容易携带。另外宝藏中还有令人瞠目结舌的重达250公斤的纯金块。 

　　在被发现后的第三天，霍克森宝藏被运到英国国家博物馆，在众多顶级考古专家专业目光的审视下它依然灿烂夺目。据考古专家研究，这是历史上古罗马钱币最集中的一次发现，也是英格兰历史上最重要的一次文物发现。宝藏的主人在紧急情况下把它们埋入地下，希望在一段时间以后重新取回，当时大约是公元440年左右。不知道是什么原因，或许是主人意外死亡，或许是他无法再找到埋藏财宝的准确位置，霍克森宝藏一直被埋藏至今。考古学家分析宝藏的主人生前地位一定显赫，可能突然遭遇变故。不过是到现在为止，他的身份还是一个谜。 

　　现在霍克森宝藏被收藏在英国国家博物馆里，为此博物馆支付了125万英镑给宝藏的发现者劳斯。虽然这些钱和宝藏的价值根本无法相比，但是劳斯很满足，他说就算一分钱都没有，他也不会后悔。宝藏的发现也为小村庄带来意外之财，很多人涌进村庄寻宝，金属探测器成为最畅销的商品。 

　　霍克森宝藏因其神秘的主人和诚实的发现者排在世界十大宝藏的第九位。

　　十、俄罗斯钻石库 

　　古代帝王总希望自己的统治能够绵延至永远，就像钻石一样坚固而恒久。这或许就是俄国沙皇彼得大帝开始收集钻石和珠宝的原因。 

　　18世纪初，彼得大帝颁布了一道保护珍宝的专项命令，要求国人不准随便变卖室中的珍贵珠宝和首饰，在一定重量以上的钻石和珠宝必须由皇家收购。另外彼得大帝还在世界范围内搜索钻石珠宝，很多小国得知他的心头所好都把本国最好的珠宝亲手献上，希望因此得到庇护和福祉。 

　　彼得大帝在自己居住的圣·彼得堡东宫内修建了一座神秘建筑物，所有收集到的珠宝都被珍藏在里面，世人称之为钻石库。彼得大帝之后，最痴迷于收集珠宝的是女皇叶卡捷琳娜二世。如果世界上每个女人都爱钻石，那么最爱钻石的女人就是叶卡捷琳娜二世。她对钻石的痴迷程度几近疯狂，每天都佩带价值连城的钻饰，而且花样经常翻新。她对钻石切割和镶嵌的工艺要求极高，俄国历史上最出色的钻石切割专家就是在叶卡捷琳娜二世时期出现的。曾经有个皇宫卫士壮着胆子称赞女皇的钻饰漂亮，他就被升官至侍卫总管。大小官员于是都把进献钻石当成最直接的升官途径。一次女皇过生日，结果在收到的上万件生日礼物中有超过一半是钻石。女皇的钻石不仅镶嵌成首饰，就连她日常用的东西都要镶满钻石。她有一本17世纪的《圣经》，银制的封面上就镶嵌了3017颗钻石。 

　　在几代皇室不停收集下，俄国的钻石库成为珍贵钻石最集中的地方，其中光世界前10位的大钻石就有3颗。 

　　最出名的是"奥尔洛夫"钻石，这是目前世界第三大钻石，重189.62克拉。17世纪初，在印度戈尔康达的钻石砂矿中发现一粒重309克拉的钻石原石，根据当时印度国王的旨意，一位钻石加工专家拟把它加工成玫瑰花模样，但未能如愿，使重量损失不少(仅磨出189.62克拉)。这颗美妙绝伦的钻石后来做了印度塞林伽神庙中婆罗门神像的眼珠。 

　　1739年，印度被波斯国王攻占之后，这颗钻石又被装饰在波斯国王宝座之上。之后钻石被盗，落入一位亚美尼亚人手中。1767年，亚美尼亚人把钻石存入了阿姆斯特丹一家银行。1772年钻石又被转手卖给了俄国御前珠宝匠伊万。伊万于1773年以40万卢布的价格又把钻石卖给了奥尔洛夫伯爵。同年，奥尔洛夫伯爵把钻石命名为"奥尔洛夫"，并把它奉献给叶卡捷琳娜二世作为她命名日的礼物。尔后"奥尔洛夫"被焊进一只雕花纯银座里，镶在了俄罗斯权杖顶端。有着传奇经历的钻饰使权杖的威严令人震慑，"奥尔洛夫"成为钻石库中最重要的藏品之一。 

　　除了"奥尔洛夫"之外，钻石库中世界级的钻石还有很多。"保罗一世"重130.35克拉，这颗紫红色美钻曾经镶嵌在印度皇冠的中央，后来被彼得大帝拥有。"波斯沙皇"重99.52克拉，曾镶嵌在波斯国王的王冠上，后来被为沙皇文狄拥有。"沙赫"虽然只重88.7克拉，但是它是世界上惟一一颗刻字的大钻石。钻石最初也是在印度被发现的，先后被两位印度国王拥有，然后辗转到波斯国王手中。钻石的3个晶面上分别刻有3个国王的名字，每次转手到新主人手中，都会被刻上新主人的名字。要知道钻石极为坚硬，要想在上面刻字难度惊人。宝石工匠从钻石上磨下一些极细的粉末，再用尖尖的细棍蘸取这种粉末给这颗钻石刻字。3次刻字之后，"沙赫"的重量从发现时的95克拉变为88.7克拉。1829年，俄国驻波斯大使被人刺死，沙皇威胁要报复。为了平息沙皇的怒火，波斯王子霍斯列夫·密尔查率代表团到圣彼得堡谢罪。王子送给沙皇一件宝物，就是这颗饱经沧桑的"沙赫"。它的价值在当时看来相当于两个国家之间的一场战争。此后，"沙赫"一直保存在俄国。 

　　单颗巨大钻石已经令世人惊叹，更何况由几千颗钻石镶嵌成的。流光溢彩的大皇冠简直是钻石的荟萃。它是1762年由宫廷珠宝匠为叶卡捷琳娜二世加冕而专门制作的，上面十几颗最重要的钻石分别是从当时欧洲国王的王冠上拆下来的。工匠在皇冠上镶嵌了4936颗钻石，共重2858克拉，整个王冠重1907克。皇冠顶端是世界上最重的尖晶石，重398.72克拉。长期以来宝石专家都认为这是一颗红宝石，后来才发现原来是稀有的尖晶石。目前这颗尖晶石是俄罗斯"必须保护的七颗宝石"之一，值得一提的是，它还是俄国人以2672金卢布从北京购买的呢。 

　　钻石库的珍宝现在已经无法用市场价格来衡量，它成为俄罗斯国家财富的象征，但即便是皇室珍宝，也有流离坎坷的时候。1914年第一次世界大战爆发后，沙皇立即下令把这些珍宝从东宫转移到莫斯科克林姆林宫。在转移途中，由于走漏消息有很大一部分珠宝流失。据一种说法，大约75%的零散钻石和宝石流入民间。 

　　二战时，俄国也流失了相当一部分珍宝，其中有号称"天字第一号珠宝盒"的琥珀大厅。1711年，普鲁士国王弗雷德里克一世下令建造一个琥珀室。琥珀室呈方形，占地约200平方米，共用了6吨琥珀，上面饰满了钻石、绿宝石和红宝石。它的价值不仅表现在财富的惊人集中，而且还是一件巴洛克艺术的杰作。5年后，弗雷德里克一世的儿子威廉一世皇帝为了庆祝普鲁士与俄国结盟，将琥珀室送给彼得大帝，彼得大帝将琥珀室收入钻石库。18世纪中叶，叶卡捷琳娜二世命令工匠对这座大厅进行装修，琥珀室成为一个富丽堂皇的大厅，变成了琥珀厅。1770年修饰最终完成时，大厅华丽得让人眼花缭乱，565枝蜡烛照亮整个大厅，烛光洒在珠宝上流光四射，令人目眩神驰......。1941年秋天，侵略苏联的德军占领了原叶卡捷琳娜二世的皇宫。希特勒下令将琥珀大厅拆散，把它们装入27个柳条箱运回德国，安放克罗列维茨市(即今天的加里宁格勒)。1943年，战局急转直下。大厅重新落入苏军之手，琥珀厅又被德国人拆卸装箱，分别藏在条顿骑士城堡和附近的防空洞里。1944年8月，盟军轰炸克罗列维茨市，将条顿骑士城堡夷为平地。从此，琥珀厅下落不明。现在无数的寻宝人在找寻琥珀大厅，但至今仍没有消息。 

　　保留下来的钻石库在克林姆林宫的地下室里尘封了8年。1922年前苏联国家委员会对这些珍宝作了鉴定，并决定由国家珍宝馆保存，现在由俄罗斯国家贵重金属宝石管理委员会管理。虽然遗失了不少珍宝，但钻石库里还有25300多克拉的钻石、1700克拉大颗粒蓝宝石、2600克拉小粒蓝宝石、2600克拉红宝石和许多又大又圆的优质精美珍珠。 

　　作为俄罗斯国家财富的集中象征，俄罗斯钻石库排在世界十大宝藏的第十位。 ]]></description>
<pubDate>
2006-04-06 08:38:44.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308136.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308136.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[如何优化C语言代码(嵌入程序员必读)]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308213.shtml</link>
<description>
<![CDATA[1、选择合适的算法和数据结构
应该熟悉算法语言，知道各种算法的优缺点，具体资料请参见相应的参考资料，有
很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找
法代替，插入排序或冒泡排序法用快速排序、合并排序或根排序代替，都可以大大
提高程序执行的效率。.选择一种合适的数据结构也很重要，比如你在一堆随机存
放的数中使用了大量的插入和删除指令，那使用链表要快得多。
数组与指针语句具有十分密码的关系，一般来说，指针比较灵活简洁，而数组则比
较直观，容易理解。对于大部分的编译器，使用指针比使用数组生成的代码更短，
执行效率更高。但是在Keil中则相反，使用数组比使用的指针生成的代码更短。。


3、使用尽量小的数据类型
能够使用字符型(char)定义的变量，就不要使用整型(int)变量来定义；能够使用
整型变量定义的变量就不要用长整型(long int)，能不使用浮点型(float)变量就
不要使用浮点型变量。当然，在定义变量后不要超过变量的作用范围，如果超过变
量的范围赋值，C编译器并不报错，但程序运行结果却错了，而且这样的错误很难
发现。
在ICCAVR中，可以在Options中设定使用printf参数，尽量使用基本型参数(%c、
%d、%x、%X、%u和%s格式说明符)，少用长整型参数(%ld、%lu、%lx和%lX格式说明
符)，至于浮点型的参数(%f)则尽量不要使用，其它C编译器也一样。在其它条件不
变的情况下，使用%f参数，会使生成的代码的数量增加很多，执行速度降低。

4、使用自加、自减指令
通常使用自加、自减指令和复合赋值表达式(如a-=1及a+=1等)都能够生成高质量的
程序代码，编译器通常都能够生成inc和dec之类的指令，而使用a=a+1或a=a-1之类
的指令，有很多C编译器都会生成二到三个字节的指令。在AVR单片适用的ICCAVR、
GCCAVR、IAR等C编译器以上几种书写方式生成的代码是一样的，也能够生成高质量
的inc和dec之类的的代码。

5、减少运算的强度
可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下：
(1)、求余运算。
    a=a%8;
可以改为：
    a=a&7;
说明：位操作只需一个指令周期即可完成，而大部分的C编译器的“%”运算均是调
用子程序来完成，代码长、执行速度慢。通常，只要求是求2n方的余数，均可使用
位操作的方法来代替。

(2)、平方运算
    a=pow(a,2.0);
可以改为：
    a=a*a;
说明：在有内置硬件乘法器的单片机中(如51系列)，乘法运算比求平方运算快得多
，因为浮点数的求平方是通过调用子程序来实现的，在自带硬件乘法器的AVR单片
机中，如ATMega163中，乘法运算只需2个时钟周期就可以完成。既使是在没有内置
硬件乘法器的AVR单片机中，乘法运算的子程序比平方运算的子程序代码短，执行
速度快。
如果是求3次方，如：
    a=pow(a,3.0);
更改为：
    a=a*a*a；
则效率的改善更明显。

(3)、用移位实现乘除法运算
    a=a*4;
    b=b/4;
可以改为：
    a=a<<2;
    b=b>>2;
说明：通常如果需要乘以或除以2n，都可以用移位的方法代替。在ICCAVR中，如果
乘以2n，都可以生成左移的代码，而乘以其它的整数或除以任何数，均调用乘除法
子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上，
只要是乘以或除以一个整数，均可以用移位的方法得到结果，如：
    a=a*9
可以改为：
    a=(a<<3)+a

6、循环
(1)、循环语
对于一些不需要循环变量参加运算的任务可以把它们放到循环外面，这里的任务包
括表达式、函数的调用、指针运算、数组访问等，应该将没有必要执行多次的操作
全部集合在一起，放到一个init的初始化程序中进行。

(2)、延时函数：
通常使用的延时函数均采用自加的形式：
    void delay (void)
    {
unsigned int i;
    for (i=0;i<1000;i++)
    ;
    }
将其改为自减延时函数：
    void delay (void)
    {
unsigned int i;
        for (i=1000;i>0;i--)
    ;
    }
两个函数的延时效果相似，但几乎所有的C编译对后一种函数生成的代码均比前一
种代码少1~3个字节，因为几乎所有的MCU均有为0转移的指令，采用后一种方式能
够生成这类指令。
在使用while循环时也一样，使用自减指令控制循环会比使用自加指令控制循环生
成的代码更少1~3个字母。
但是在循环中有通过循环变量“i”读写数组的指令时，使用预减循环时有可能使
数组超界，要引起注意。

(3)while循环和do…while循环
用while循环时有以下两种循环形式：
unsigned int i;
    i=0;
    while (i<1000)
    {
        i++;
   //用户程序
    }
或：
unsigned int i;
    i=1000;
    do
    i--;
    //用户程序
    while (i>0);
在这两种循环中，使用do…while循环编译后生成的代码的长度短于while循环。

7、查表
在程序中一般不进行非常复杂的运算，如浮点数的乘除及开方等，以及一些复杂的
数学模型的插补运算，对这些即消耗时间又消费资源的运算，应尽量使用查表的方
式，并且将数据表置于程序存储区。如果直接生成所需的表比较困难，也尽量在启
了，减少了程序执行过程中重复计算的工作量。

8、其它
比如使用在线汇编及将字符串和一些常量保存在程序存储器中，均有利于优化]]></description>
<pubDate>
2006-03-29 14:03:43.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308213.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308213.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[simple uclinux app]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308298.shtml</link>
<description>
<![CDATA[ 1、建立工作目录
    mkdir hello 
    cd hello 
    2、编写程序源代码
    在LINUX 下的文本编辑器有许多，常用的是vim, Xwindow 界面下的gedit 等，我们在开发过程中推荐使用vim ，用户需要学习vim 的操作方法，请参考《uClinux 开发指南》中的关于vim 的操作指南。
    实际的源代码较简单，如下：
    #include  
    main() 
    { 
    printf(“hello world \n”); 
    } 
    3、编写Makefile 
    CC=arm-uclibc-gcc 
    EXEC = hello 
    OBJS = hello.o 
    CFLAGS += 
    LDFLAGS+=-elf2flt –static 
    all: ＄(EXEC) 
    ＄(EXEC): ＄(OBJS) 
    ＄(CC) ＄(LDFLAGS) -o ＄@ ＄(OBJS) 
    clean: 
    -rm -f ＄(EXEC) *.elf *.gdb *.o 
    这个makefile 显示了几个主要的部分：
    z CC 指明编译器的宏。
    z EXEC 表示生成的执行文件名称的宏。
    z OBJS 目标文件列表宏。
    z CFLAGS 编译参数宏。
    z LDFLAGS 连接参数宏。
    z all: 编译主入口。
    z clean ：清除编译结果节。
    4、编译应用程序
    在hello 目录下运行make ，如果进行了修改，重新编译则运行: 
    make clean 
    make 
    5、增加下面的内容到../user/Makefile中: 
    dir_＄(CONFIG_USER_MYAPP_DEMO) += myapp 
    它的作用是让编译器可以访问到您创建的MyApp目录的Makefile. 
    6、编辑文件 config/Configure.help, 增加下面的行 
    CONFIG_USER_ HELLOWORLD _HELLO 
    A example C program. This Program print "helo, wellcom to S3CEB4510!" on screen. 
    7、编辑文件 config/config.in 在 ‘’后添加新的部分 
    ############################################################################# 
    mainmenu_option next_comment 
    comment 'heloworld' 
    bool 'helo' CONFIG_USER_HELLOWORLD_HELLO 
    endmenu 
    ############################################################################# 
    8、make menuconfig 
    9、重新编译生成： 
    make dep 
    make clean 
    make lib_only ；如果没有改动，不必要每次都来 
    make user_only ；每次修改过应用程序之后，本行以及步后面的都必须重来 
    make romfs 
    make image 
    make 
    10、下载 image.ram 到S3CEB4510,即可看到运行效果 ]]></description>
<pubDate>
2006-03-27 18:18:36.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308298.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308298.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[Linux 系统内核的调试]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308340.shtml</link>
<description>
<![CDATA[调试是软件开发过程中一个必不可少的环节，在 Linux 内核开发的过程中也不可避免地会面对如何调试内核的问题。但是，Linux 系统的开发者出于保证内核代码正确性的考虑，不愿意在 Linux 内核源代码树中加入一个调试器。他们认为内核中的调试器会误导开发者，从而引入不良的修正[1]。所以对 Linux 内核进行调试一直是个令内核程序员感到棘手的问题，调试工作的艰苦性是内核级的开发区别于用户级开发的一个显著特点。

尽管缺乏一种内置的调试内核的有效方法，但是 Linux 系统在内核发展的过程中也逐渐形成了一些监视内核代码和错误跟踪的技术。同时，许多的补丁程序应运而生，它们为标准内核附加了内核调试的支持。尽管这些补丁有些并不被 Linux 官方组织认可，但他们确实功能完善，十分强大。调试内核问题时，利用这些工具与方法跟踪内核执行情况，并查看其内存和数据结构将是非常有用的。
本文将首先介绍 Linux 内核上的一些内核代码监视和错误跟踪技术，这些调试和跟踪方法因所要求的使用环境和使用方法而各有不同，然后重点介绍三种 Linux 内核的源代码级的调试方法。
1. Linux 系统内核级软件的调试技术
printk() 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk() 调试调用，可以直接把所关心的信息打打印到屏幕上，从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器（Linux kernel debugger，kdb）是 Linux 内核的补丁，它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍，大家可以参考。 Kprobes 提供了一个强行进入任何内核例程，并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息，而无需对Linux内核频繁编译和启动，具体使用方法，请参考使用 Kprobes 调试内核。
以上介绍了进行Linux内核调试和跟踪时的常用技术和方法。当然，内核调试与跟踪的方法还不止以上提到的这些。这些调试技术的一个共同的特点在于，他们都不能提供源代码级的有效的内核调试手段，有些只能称之为错误跟踪技术，因此这些方法都只能提供有限的调试能力。下面将介绍三种实用的源代码级的内核调试方法。
2. 使用KGDB构建Linux内核调试环境
kgdb 提供了一种使用 gdb调试 Linux 内核的机制。使用KGDB可以象调试普通的应用程序那样，在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器，一台作为开发机（Development Machine）,另一台作为目标机（Target Machine），两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆，在其内部两端的第2脚（TXD）与第3脚（RXD）交叉相连，第7脚（接地脚）直接相连。调试过程中，被调试的内核运行在目标机上，gdb调试器运行在开发机上。
目前，kgdb发布支持i386、x86_64、32-bit PPC、SPARC等几种体系结构的调试器。有关kgdb补丁的下载地址见参考资料[4]。
2．1 kgdb的调试原理
安装kgdb调试环境需要为Linux内核应用kgdb补丁，补丁实现的gdb远程调试所需要的功能包括命令处理、陷阱处理及串口通讯3个主要的部分。 kgdb补丁的主要作用是在Linux内核中添加了一个调试Stub。调试Stub是Linux内核中的一小段代码，提供了运行gdb的开发机和所调试内核之间的一个媒介。gdb和调试stub之间通过gdb串行协议进行通讯。gdb串行协议是一种基于消息的ASCII码协议，包含了各种调试命令。当设置断点时，kgdb负责在设置断点的指令前增加一条trap指令，当执行到断点时控制权就转移到调试stub中去。此时，调试stub的任务就是使用远程串行通信协议将当前环境传送给gdb，然后从gdb处接受命令。gdb命令告诉stub下一步该做什么，当stub收到继续执行的命令时，将恢复程序的运行环境，把对CPU的控制权重新交还给内核。



2．2 Kgdb的安装与设置
下面我们将以Linux 2.6.7内核为例详细介绍kgdb调试环境的建立过程。
2.2.1软硬件准备
以下软硬件配置取自笔者进行试验的系统配置情况：



kgdb补丁的版本遵循如下命名模式：Linux-A-kgdb-B，其中A表示Linux的内核版本号，B为kgdb的版本号。以试验使用的kgdb补丁为例，linux内核的版本为linux-2.6.7，补丁版本为kgdb-2.2。
物理连接好串口线后，使用以下命令来测试两台机器之间串口连接情况，stty命令可以对串口参数进行设置：
在development机上执行：

stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
在target机上执行：

stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
在developement机上执行：

echo hello > /dev/ttyS0
在target机上执行：

cat /dev/ttyS0
如果串口连接没问题的话在将在target机的屏幕上显示"hello"。
2．2．2 安装与配置
下面我们需要应用kgdb补丁到Linux内核，设置内核选项并编译内核。这方面的资料相对较少，笔者这里给出详细的介绍。下面的工作在开发机（developement）上进行，以上面介绍的试验环境为例，某些具体步骤在实际的环境中可能要做适当的改动：
I、内核的配置与编译

[root@lisl tmp]# tar -jxvf linux-2.6.7.tar.bz2
[root@lisl tmp]#tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar
[root@lisl tmp]#cd inux-2.6.7
请参照目录补丁包中文件README给出的说明，执行对应体系结构的补丁程序。由于试验在i386体系结构上完成，所以只需要安装一下补丁：core-lite.patch、i386-lite.patch、 8250.patch、eth.patch、core.patch、i386.patch。应用补丁文件时，请遵循kgdb软件包内series文件所指定的顺序，否则可能会带来预想不到的问题。eth.patch文件是选择以太网口作为调试的连接端口时需要运用的补丁
。
应用补丁的命令如下所示：

[root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch 
如果内核正确，那么应用补丁时应该不会出现任何问题（不会产生*.rej文件）。为Linux内核添加了补丁之后，需要进行内核的配置。内核的配置可以按照你的习惯选择配置Linux内核的任意一种方式。

[root@lisl tmp]#make menuconfig
在内核配置菜单的Kernel hacking选项中选择kgdb调试项，例如：

 [*] KGDB: kernel debugging with remote gdb                                                             
       Method for KGDB communication (KGDB: On generic serial port (8250)) ---> 
 [*] KGDB: Thread analysis                                                                            
 [*] KGDB: Console messages through gdb
[root@lisl tmp]#make
 
编译内核之前请注意Linux目录下Makefile中的优化选项，默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下，编译器要对内核中的某些代码的执行顺序进行改动，所以在调试时会出现程序运行与代码顺序不一致的情况。可以把Makefile中的-O2选项改为-O,但不可去掉- O，否则编译会出问题。为了使编译后的内核带有调试信息，注意在编译内核的时候需要加上-g选项。
不过，当选择"Kernel debugging->Compile the kernel with debug info"选项后配置系统将自动打开调试选项。另外，选择"kernel debugging with remote gdb"后，配置系统将自动打开"Compile the kernel with debug info"选项。
内核编译完成后，使用scp命令进行将相关文件拷贝到target机上(当然也可以使用其它的网络工具，如rcp)。

[root@lisl tmp]#scp arch/i386/boot/bzImage root@192.168.6.13:/boot/vmlinuz-2.6.7-kgdb
[root@lisl tmp]#scp System.map root@192.168.6.13:/boot/System.map-2.6.7-kgdb
如果系统启动使所需要的某些设备驱动没有编译进内核的情况下，那么还需要执行如下操作：

[root@lisl tmp]#mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7
[root@lisl tmp]#scp initrd-2.6.7-kgdb root@192.168.6.13:/boot/ initrd-2.6.7-kgdb
II、kgdb的启动
在将编译出的内核拷贝的到target机器之后，需要配置系统引导程序，加入内核的启动选项。以下是kgdb内核引导参数的说明：



如表中所述，在kgdb 2.0版本之后内核的引导参数已经与以前的版本有所不同。使用grub引导程序时，直接将kgdb参数作为内核vmlinuz的引导参数。下面给出引导器的配置示例。

title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
在使用lilo作为引导程序时，需要把kgdb参放在由append修饰的语句中。下面给出使用lilo作为引导器时的配置示例。

image=/boot/vmlinuz-2.6.7-kgdb
label=kgdb
    read-only
    root=/dev/hda3
append="gdb gdbttyS=1 gdbbaud=115200"
保存好以上配置后重新启动计算机，选择启动带调试信息的内核，内核将在短暂的运行后在创建init内核线程之前停下来，打印出以下信息，并等待开发机的连接。
Waiting for connection from remote gdb...
在开发机上执行：

gdb
file vmlinux
set remotebaud 115200
target remote /dev/ttyS0
其中vmlinux是指向源代码目录下编译出来的Linux内核文件的链接，它是没有经过压缩的内核文件，gdb程序从该文件中得到各种符号地址信息。
这样，就与目标机上的kgdb调试接口建立了联系。一旦建立联接之后，对Linux内的调试工作与对普通的运用程序的调试就没有什么区别了。任何时候都可以通过键入ctrl+c打断目标机的执行，进行具体的调试工作。
在kgdb 2.0之前的版本中，编译内核后在arch/i386/kernel目录下还会生成可执行文件gdbstart。将该文件拷贝到target机器的/boot目录下，此时无需更改内核的启动配置文件，直接使用命令：

[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
可以在KGDB内核引导启动完成后建立开发机与目标机之间的调试联系。
2．2．3 通过网络接口进行调试
kgdb也支持使用以太网接口作为调试器的连接端口。在对Linux内核应用补丁包时，需应用eth.patch补丁文件。配置内核时在Kernel hacking中选择kgdb调试项，配置kgdb调试端口为以太网接口，例如：

[*]KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On ethernet) ---> 
( ) KGDB: On generic serial port (8250)
(X) KGDB: On ethernet
另外使用eth0网口作为调试端口时，grub.list的配置如下：

title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.
5.13/,@192.168. 6.13/ 
其他的过程与使用串口作为连接端口时的设置过程相同。
注意：尽管可以使用以太网口作为kgdb的调试端口，使用串口作为连接端口更加简单易行，kgdb项目组推荐使用串口作为调试端口。
2．2．4 模块的调试方法
内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的，所以develop机的gdb无法得到各种符号地址信息。所以，使用kgdb调试模块所需要解决的一个问题是，需要通过某种方法获得可加载模块的最终加载地址信息，并把这些信息加入到gdb环境中。
I、在Linux 2.4内核中的内核模块调试方法
在Linux2.4.x内核中，可以使用insmod -m命令输出模块的加载信息，例如：

[root@lisl tmp]# insmod -m hello.ko >modaddr
查看模块加载信息文件modaddr如下：

.this           00000060 c88d8000 2**2
.text           00000035 c88d8060 2**2
.rodata         00000069 c88d80a0 2**5
……
.data           00000000 c88d833c 2**2
.bss            00000000 c88d833c 2**2
……
在这些信息中，我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。

(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s 
.rodata 0xc88d80a0 -s .bss 0x c88d833c
这种方法也存在一定的不足，它不能调试模块初始化的代码，因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址，更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。
在target 机上用上述方法得到模块加载的地址信息，然后再用rmmod卸载模块。在development机上将得到的模块地址信息导入到gdb环境中，在内核代码的调用初始化代码之前设置断点。这样，在target机上再次插入模块时，代码将在执行模块初始化之前停下来，这样就可以使用gdb命令调试模块初始化代码了。
另外一种调试模块初始化函数的方法是：当插入内核模块时，内核模块机制将调用函数sys_init_module(kernel/modle.c)执行对内核模块的初始化，该函数将调用所插入模块的初始化函数。程序代码片断如下：

…… ……
   if (mod->init != NULL)
      ret = mod->init();
…… ……
在该语句上设置断点，也能在执行模块初始化之前停下来。
II、在Linux 2.6.x内核中的内核模块调试方法
Linux 2.6之后的内核中，由于module-init-tools工具的更改，insmod命令不再支持-m参数，只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式，我们知道程序中各段的意义如下：
.text（代码段）：用来存放可执行文件的操作指令，也就是说是它是可执行程序在内存种的镜像。
.data（数据段）：数据段用来存放可执行文件中已初始化全局变量，也就是存放程序静态分配的变量和全局变量。
.bss（BSS段）：BSS段包含了程序中未初始化全局变量，在内存中 bss段全部置零。
.rodata（只读段）：该段保存着只读数据，在进程映象中构造不可写的段。
通过在模块初始化函数中放置一下代码，我们可以很容易地获得模块加载到内存中的地址。

……
int bss_var;
static int hello_init(void)
{
printk(KERN_ALERT "Text location .text(Code Segment):%p\n",hello_init);

static int data_var=0;
printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&data_var);

printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var);
……
}
Module_init(hello_init);
这里，通过在模块的初始化函数中添加一段简单的程序，使模块在加载时打印出在内核中的加载地址。.rodata段的地址可以通过执行命令readelf -e hello.ko，取得.rodata在文件中的偏移量并加上段的align值得出。
为了使读者能够更好地进行模块的调试，kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似，更多的信息请阅读参考资料[4]。
2．2．5 硬件断点
kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点：执行断点（Execution Breakpoint）、写断点（Write Breakpoint）、访问断点（Access Breakpoint）但不支持I/O访问的断点。目前，kgdb对硬件断点的支持是通过宏来实现的，最多可以设置4个硬件断点，这些宏的用法如下：



在有些情况下，硬件断点的使用对于内核的调试是非常方便的。有关硬件断点的定义和具体的使用说明见参考资料[4]
。
2．3．在VMware中搭建调试环境
kgdb调试环境需要使用两台微机分别充当development机和target机，使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。以windows下的环境为例，创建两台虚拟机，一台作为开发机，一台作为目标机。
2．3．1虚拟机之间的串口连接
虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上，例如开发机连接到COM1，目标机连接到COM2，然后把两个串口通过串口线相连接。另一种更为简便的方法是：在较高一些版本的VMware中都支持把串口映射到命名管道，把两个虚拟机的串口映射到同一个命名管道。例如，在两个虚拟机中都选定同一个命名管道 \\.\pipe\com_1,指定target机的COM口为server端，并选择"The other end is a virtual machine"属性；指定development机的COM口端为client端，同样指定COM口的"The other end is a virtual machine"属性。对于IO mode属性，在target上选中"Yield CPU on poll"复选择框，development机不选。这样，可以无需附加任何硬件，利用虚拟机就可以搭建kgdb调试环境。即降低了使用kgdb进行调试的硬件要求，也简化了建立调试环境的过程。



2．3．2 VMware的使用技巧
VMware 虚拟机是比较占用资源的，尤其是象上面那样在Windows中使用两台虚拟机。因此，最好为系统配备512M以上的内存，每台虚拟机至少分配128M的内存。这样的硬件要求，对目前主流配置的PC而言并不是过高的要求。出于系统性能的考虑，在VMware中尽量使用字符界面进行调试工作。同时，Linux 系统默认情况下开启了sshd服务，建议使用SecureCRT登陆到Linux进行操作，这样可以有较好的用户使用界面。
2．3．3 在Linux下的虚拟机中使用kgdb
对于在Linux下面使用VMware虚拟机的情况，笔者没有做过实际的探索。从原理上而言，只需要在Linux下只要创建一台虚拟机作为target机，开发机的工作可以在实际的Linux环境中进行，搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机，所以使用Linux下的虚拟机搭建 kgdb调试环境对系统性能的要求较低。（vmware已经推出了Linux下的版本）还可以在development机上配合使用一些其他的调试工具，例如功能更强大的cgdb、图形界面的DDD调试器等，以方便内核的调试工作。



2．4 kgdb的一些特点和不足
使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高，必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境，但是对系统其他方面的性能也提出了一定的要求，同时也增加了搭建调试环境时复杂程度。另外，kgdb内核的编译、配置也比较复杂，需要一定的技巧，笔者当时做的时候也是费了很多周折。当调试过程结束后时，还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试，也就是说kgdb并不能用于调试系统一开始的初始化引导过程。
不过，kgdb是一个不错的内核调试工具，使用它可以进行对内核的全面调试，甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下，对内核的调试将更方便。
3. 使用SkyEye构建Linux内核调试环境
SkyEye 是一个开源软件项目（OPenSource Software）,SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台，可以模拟多种嵌入式开发板，支持多种CPU指令集。SkyEye 的核心是 GNU 的 gdb 项目，它把gdb和 ARM Simulator很好地结合在了一起。加入ARMulator 的功能之后，它就可以来仿真嵌入式开发板，在它上面不仅可以调试硬件驱动，还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。
3．1 SkyEye的安装和μcLinux内核编译
3．1.1 SkyEye的安装
SkyEye 的安装不是本文要介绍的重点，目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11]。由于skyeye面目主要用于嵌入式系统领域，所以在skyeye上经常使用的是μcLinux系统，当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍 μcLinux 2.6在skyeye上编译的相关资料并不多，所以下面进行详细介绍。
3．1.2 μcLinux 2.6.x的编译
要在SkyEye中调试操作系统内核，首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此，正确编译待调试操作系统内核并配置 SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于Atmel AT91X40的开发板，并运行μcLinux 2.6为例介绍SkyEye的具体调试方法。
I、安装交叉编译环境
先安装交叉编译器。尽管在一些资料中说明使用工具链arm -elf-tools-20040427.sh ,但是由于arm-elf-xxx与arm-linux-xxx对宏及链接处理的不同，经验证明使用arm-elf-xxx工具链在链接vmlinux的最后阶段将会出错。所以这里我们使用的交叉编译工具链是：arm-uclinux-tools-base-gcc3.4.0-20040713.sh，关于该交叉编译工具链的下载地址请参见[6]。注意以下步骤最好用root用户来执行。

[root@lisl tmp]#chmod +x arm-uclinux-tools-base-gcc3.4.0-20040713.sh
[root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh
安装交叉编译工具链之后，请确保工具链安装路径存在于系统PATH变量中。
II、制作μcLinux内核
得到μcLinux发布包的一个最容易的方法是直接访问uClinux.org站点[7]。该站点发布的内核版本可能不是最新的，但你能找到一个最新的 μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里，将使用这种方法来制作最新的μcLinux内核。目前（笔者记录编写此文章时），所能得到的发布包的最新版本是uClinux-dist.20041215.tar.gz。
下载uClinux-dist.20041215.tar.gz，文件的下载地址请参见[7]。
下载linux-2.6.9-hsc0.patch.gz，文件的下载地址请参见[8]。
下载linux-2.6.9.tar.bz2，文件的下载地址请参见[9]。
现在我们得到了整个的linux-2.6.9源代码，以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。

[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2
[root@lisl uClinux-dist]# tar -jxvf linux-2.6.9.tar.bz2
[root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0 
或者使用：

[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz 
[root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch
执行以上过程后，将在linux-2.6.9/arch目录下生成一个补丁目录－armnommu。删除原来μcLinux目录里的linux-2.6.x(即那个linux-2.6.9-uc0)，并将我们打好补丁的Linux内核目录更名为linux-2.6.x。

[root@lisl uClinux-dist]# rm -rf linux-2.6.x/
[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x
III、配置和编译μcLinux内核
因为只是出于调试μcLinux内核的目的，这里没有生成uClibc库文件及romfs.img文件。在发布μcLinux时，已经预置了某些常用嵌入式开发板的配置文件，因此这里直接使用这些配置文件，过程如下：

[root@lisl uClinux-dist]# cd linux-2.6.x
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_
deconfig
atmel_deconfig文件是μcLinux发布时提供的一个配置文件，存放于目录linux-2.6.x /arch/armnommu/configs/中。

[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux-
oldconfig
下面编译配置好的内核：

[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1
一般情况下，编译将顺利结束并在Linux-2.6.x/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核，需要打开内核编译的调试选项-g，使编译后的内核带有调试信息。打开编译选项的方法可以选择：
"Kernel debugging->Compile the kernel with debug info"后将自动打开调试选项。也可以直接修改linux-2.6.x目录下的Makefile文件，为其打开调试开关。方法如下：。

CFLAGS += -g 
最容易出现的问题是找不到arm-uclinux-gcc命令的错误，主要原因是PATH变量中没有包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下，它的安装目录是/root/bin/arm-linux-tool/，使用以下命令将路径加到PATH环境变量中。

Export PATH＝＄PATH:/root/bin/arm-linux-tool/bin
IV、根文件系统的制作
Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式系统使用的所有应用程序、库文件及其他一些需要用到的服务。出于文章篇幅的考虑，这里不打算介绍根文件系统的制作方法，读者可以查阅一些其他的相关资料。值得注意的是，由配置文件skyeye.conf指定了装载到内核中的根文件系统。
3．2 使用SkyEye调试
编译完μcLinux内核后，就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。
需要提醒读者的是，SkyEye的配置文件－skyeye.conf记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件，很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时，请首先检查该配置文件然后再进行其他的工作。此时，所有的准备工作已经完成，就可以进行内核的调试工作了。
3．3使用SkyEye调试内核的特点和不足
在SkyEye 中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU，因此一般而言需要使用交叉编译工具编译待调试的 Linux系统内核。另外，制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过，当调试过程结束后无需重新制作所要发布的内核。
SkyEye只是对系统硬件进行了一定程度上的模拟，所以在SkyEye与真实硬件环境相比较而言还是有一定的差距，这对一些与硬件紧密相关的调试可能会有一定的影响，例如驱动程序的调试。不过对于大部分软件的调试，SkyEye已经提供了精度足够的模拟了。
SkyEye的下一个目标是和eclipse结合，有了图形界面，能为调试和查看源码提供一些方便。
4. 使用UML调试Linux内核
User-mode Linux（UML）简单说来就是在Linux内运行的Linux。该项目是使Linux内核成为一个运行在 Linux 系统之上单独的、用户空间的进程。UML并不是运行在某种新的硬件体系结构之上，而是运行在基于 Linux 系统调用接口所实现的虚拟机。正是由于UML是一个将Linux作为用户空间进程运行的特性，可以使用UML来进行操作系统内核的调试。有关UML的介绍请查阅参考资料[10]、[12]。
4．1 UML的安装与调试
UML 的安装需要一台运行Linux 2.2.15以上，或者2.3.22以上的I386机器。对于2.6.8及其以前版本的UML，采用两种形式发布：一种是以RPM包的形式发布，一种是以源代码的形式提供UML的安装。按照UML的说明，以RPM形式提供的安装包比较陈旧且会有许多问题。以二进制形式发布的UML包并不包含所需要的调试信息，这些代码在发布时已经做了程度不同的优化。所以，要想利用UML调试Linux系统内核，需要使用最新的UML patch代码和对应版本的Linux内核编译、安装UML。完成UML的补丁之后，会在arch目录下产生一个um目录，主要的UML代码都放在该目录下。
从2.6.9版本之后（包含2.6.9版本的Linux），User-Mode Linux已经随Linux内核源代码树一起发布，它存放于arch/um目录下。
编译好UML的内核之后，直接使用gdb运行已经编译好的内核即可进行调试。
4．2使用UML调试系统内核的特点和不足
目前，用户模式 Linux 虚拟机也存在一定的局限性。由于UML虚拟机是基于Linux系统调用接口的方式实现的虚拟机，所以用户模式内核不能访问主机系统上的硬件设备。因此， UML并不适合于调试那些处理实际硬件的驱动程序。不过，如果所编写的内核程序不是硬件驱动，例如Linux文件系统、协议栈等情况，使用UML作为调试工具还是一个不错的选择。
5. 内核调试配置选项
为了方便调试和测试代码，内核提供了许多与内核调试相关的配置选项。这些选项大部分都在内核配置编辑器的内核开发(kernel hacking)菜单项中。在内核配置目录树菜单的其他地方也还有一些可配置的调试选项，下面将对他们作一定的介绍。
Page alloc debugging ：CONFIG_DEBUG_PAGEALLOC: 
不使用该选项时，释放的内存页将从内核地址空间中移出。使用该选项后，内核推迟移出内存页的过程，因此能够发现内存泄漏的错误。
Debug memory allocations ：CONFIG_DEBUG_SLAB: 
该打开该选项时，在内核执行内存分配之前将执行多种类型检查，通过这些类型检查可以发现诸如内核过量分配或者未初始化等错误。内核将会在每次分配内存前后时设置一些警戒值，如果这些值发生了变化那么内核就会知道内存已经被操作过并给出明确的提示，从而使各种隐晦的错误变得容易被跟踪。
Spinlock debugging ：CONFIG_DEBUG_SPINLOCK:
打开此选项时，内核将能够发现spinlock未初始化及各种其他的错误,能用于排除一些死锁引起的错误。
Sleep-inside-spinlock checking：CONFIG_DEBUG_SPINLOCK_SLEEP: 
打开该选项时，当spinlock的持有者要睡眠时会执行相应的检查。实际上即使调用者目前没有睡眠，而只是存在睡眠的可能性时也会给出提示。
Compile the kernel with debug info ：CONFIG_DEBUG_INFO:
打开该选项时，编译出的内核将会包含全部的调试信息，使用gdb时需要这些调试信息。
Stack utilization instrumentation ：CONFIG_DEBUG_STACK_USAGE:
该选项用于跟踪内核栈的溢出错误，一个内核栈溢出错误的明显的现象是产生oops错误却没有列出系统的调用栈信息。该选项将使内核进行栈溢出检查，并使内核进行栈使用的统计。
Driver Core verbose debug messages：CONFIG_DEBUG_DRIVER:
该选项位于"Device drivers-> Generic Driver Options"下，打开该选项使得内核驱动核心产生大量的调试信息，并将他们记录到系统日志中。
Verbose SCSI error reporting (kernel size +=12K) ：CONFIG_SCSI_CONSTANTS:
该选项位于"Device drivers/SCSI device support"下。当SCSI设备出错时内核将给出详细的出错信息。
Event debugging：CONFIG_INPUT_EVBUG: 
打开该选项时，会将输入子系统的错误及所有事件都输出到系统日志中。该选项在产生了详细的输入报告的同时，也会导致一定的安全问题。
以上内核编译选项需要读者根据自己所进行的内核编程的实际情况，灵活选取。在使用以上介绍的三种源代码级的内核调试工具时，一般需要选取CONFIG_DEBUG_INFO选项，以使编译的内核包含调试信息。
6. 总结
上面介绍了一些调试Linux内核的方法，特别是详细介绍了三种源代码级的内核调试工具，以及搭建这些内核调试环境的方法，读者可以根据自己的情况从中作出选择。
调试工具（例如gdb）的运行都需要操作系统的支持，而此时内核由于一些错误的代码而不能正确执行对系统的管理功能，所以对内核的调试必须采取一些特殊的方法进行。以上介绍的三种源代码级的调试方法，可以归纳为以下两种策略：
I、为内核增加调试Stub，利用调试Stub进行远程调试，这种调试策略需要target及development机器才能完成调试任务。
II、将虚拟机技术与调试工具相结合，使Linux内核在虚拟机中运行从而利用调试器对内核进行调试。这种策略需要制作适合在虚拟机中运行的系统内核。
由不同的调试策略决定了进行调试时不同的工作原理，同时也形成了各种调试方法不同的软硬件需求和各自的特点。
另外，需要说明的是内核调试能力的掌握很大程度上取决于经验和对整个操作系统的深入理解。对系统内核的全面深入的理解，将能在很大程度上加快对Linux系统内核的开发和调试。
对系统内核的调试技术和方法绝不止上面介绍所涉及的内容，这里只是介绍了一些经常看到和听到方法。在Linux内核向前发展的同时，内核的调试技术也在不断的进步。希望以上介绍的一些方法能对读者开发和学习Linux有所帮助。
参考资料
[1] http://oss.sgi.com/projects/kdb/
[2] http://www-128.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html
[3] http://www-128.ibm.com/developerworks/cn/linux/l-kdbug/
[4] http://www-128.ibm.com/developerworks/cn/linux/l-kprobes.html
[5] http://kgdb.linsyssoft.com/downloads.htm
[6] ftp://166.111.68.183
[7] http://adam.kaist.ac.kr/~hschoe/dow...ols-20040427.sh
[8] http://www.uclinux.org/pub/uClinux/dist/
[9] http://adam.kaist.ac.kr/~hschoe/dow...5-hsc2.patch.gz
[10] http:// www.kernel.org
[11] http://user-mode-linux.sourceforge.net/
[12] http://www-128.ibm.com/developerworks/cn/linux/l-skyeye/part1/
[13] http://www-128.ibm.com/developerworks/cn/views/linux/tutorials.jsp?cv_doc_id=84978
参考文献
[1]Robert Love Linux kernel development机械工业出版社
[2]陈渝源代码开发的嵌入式系统软件分析与实践北京航空航天大学出版社
[3]Alessandro Rubini Linux device driver 2se Edition O'Reilly
[4]Jonathan Corbet Linux device driver 3rd Edition O'Reilly
[5]李善平 Linux内核源代码分析大全机械工业出版社]]></description>
<pubDate>
2006-03-27 16:54:49.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308340.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308340.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[自己动手打造嵌入式Linux软硬件开发环境]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308383.shtml</link>
<description>
<![CDATA[from (http://www.dailzh.net)

Linux和uClinux
1991年8月，芬兰的一个学生在comp.os.minix新闻组贴上了以下这段话：

你好所有使用minix的人-我正在为386(486)AT做一个免费的操作系统(只是为了爱好,不会象gnu那样很大很专业. 

这名学生就是Linus Torvalds, 而他所说的'爱好'就变成我们今天知道的Linux。

由于Linux的源代码公布在互联网上，可以免费得到，因此从一开始就吸引了世界各地的UNIX 行家为Linux 编写了

大量的驱动程序和应用软件，在短短几年时间里，Linux 就发展成为一个相当完善的操作系统，而且Linux支持的硬件

平台是所有操作系统中最多的，目前Linux支持硬件平台：Intel的IA64、Compaq的Alpha、Sun的Sparc/Sparc64、SGI

的Mips、IBM的S396、ARM、PowerPC等。Linux更大的影响在于它正逐渐地应用于嵌入式设备，uClinux正是在这种氛围

下产生的。uClinux就是Micro-Control-Linux，它也是一个开放源码的项目，uClinux的源代码和开发工具可以免费从

http://www.uclinux.org上下载得到。
uClinux是专为那些没有MMU(内存管理单元)的嵌入式处理器开发的，和主流的Linux相比，uClinux有以下的特点

：
1．简化了内核加载方式，uClinux的内核可以在Flash上直接运行：就是把uClinux的内核的可执行映象烧写

到flash上，系统启动时从Flash的某个地址开始逐句执行；也可以加载到内存中运行：把内核的压缩文件存放在Flash

上，系统启动时读取压缩文件在内存里解压，然后开始执行。
2．采用了romfs文件系统作为root文件系统：这种文件系统相对于一般的ext2文件系统要求更少的空间，首先内核

支持romfs文件系统比支持ext2文件系统需要更少的代码，其次romfs文件系统相对简单，建立文件系统superblock需要

更少的存储空间。Romfs文件系统不支持动态擦写，对于系统需要动态保存的数据采用RAM盘的方法处理，RAM盘采

用ext2文件系统。
3．使用了Flat可执行文件格式：elf格式有很大的文件头，flat文件对文件头和一些段信息做了简化。
4．重写了应用程序库： uClibc对libc做了精简，uClinux对用户程序采用静态连接的形式。

uClinux的开发环境
www.uclinux.org为uClinux提供了GNU的交叉编译器，包括以下组件：Gcc交叉编译器，即在宿主机上开发编译目

标上可运行的二进制文件；Binutils辅助工具，包括objdump、as、ld等；Gdb调试器。以在ARM7上开发uClinux为例：
1．获得uClinux-dist的源码www.uclinux.org上定期为新推出的Linux内核推出相应的源码包，最新的版本为

Kernel-2.4.21,可以从http://www.uclinux.org/pub/uClinux/dist/ 上免费下载得到。这个源码包里包含

了uCLinux-2.4.21、uClibc和已经移植到uClinux下的用户应用程序。下载完后，会得到一

个uClinux-dist-20030522.tar.gz的文件，把它保存到/home目录下，然后执行：tar zxvf 

uClinux-dist-20030522.tar.gz，当tar程序运行完毕后，在/home目录下会有一个/home/uClinux-dist的新目录，这
个目录就是uClinux的源码根目录，里面有进行uClinux开发的所有的源代码。
2．获得ARM开发工www.uclinux.org提供uClinux源码的同时还提供相应的交叉编译工具。要在开发主机上

为ARM7目标系统编译uClinux，还需要从http://www.uclinux.org/pub/uClinux/arm-elf-tools/ 上下载ARM交叉编译

器：arm-elf-tools-20030314.sh。得到这个文件以后，执行以下命令：sh arm-elf-tools-20030314.sh，这个命令

会在开发主机上自动建立一个uClinux-ARM的交叉编译环境。键入arm-elf-gcc, 如果能看到下面的输出信息:
Reading specs from /usr/local/lib/gcc-lib/arm-elf/2.95.3/specsgcc version 2.95.3 20010315 (release)
(ColdFire patches - 20010318 from http://fiddes.net/coldfire/)(uClinux XIP and shared lib patches 
from http://www.snapgear.com/)
表示uClinux-ARM的交叉编译环境已经建立起来了。

现在开发主机里已经有了uClinux的源代码和编译这些源代码的工具，也可以用make menuconfig, make等命令来

编译uClinux和用户程序，为ARM目标板编译了一个内核映像文件，接下来要做得是需要一块ARM7的开发板来运行这个映

像文件（关于如何编译uClinux和用户程序请www.uClinux.org上得相关文档）。

构建ARM7-uClinux开发板
uClinux只需要极少的硬件资源就可运行起来，以ARM7TDMI为例，只需要以下硬件：
1．CPU – Samsung S3C4510B
2．SDRAM 8M以上
3．一个简易的串口
4．2M Flash
5．一个以太网接口（可选）
目前各嵌入式微处理器的厂商在推出每款处理器的同时都会提供一个Demo板，供用户来测试微处理器的性

能。Samsung公司对S3C4510B处理器提供了一款SNDS100的Demo板。Demo板的原理图可以从Samsung公司的网站上免费下

载，对这个原理图作一些修改，只保留上面列出的5个部分，去掉其他多余的部分。修改以后的原理图就是一个能够运行

uClinux的ARM7目标板原理图，然后根据这个原理图去加工几张PCB板，焊上相应的元件，一块能运行uClinux的ARM7开

发板就做成了（这款开发板相应得原理图、PCB图可以从http://www.dailzh.net上免费下载得到）。
慢着，虽然这块开发板已经焊接完成，但目前它只是一堆电子零件的简单组合，要在它上面跑uClinux，还需要相

应的软件来管理这些硬件。
前面提到uClinux可以从Flash中直接运行，就是说可以将uClinux的映像文件直接烧写到Flash中，然后上

电，uClinux会从Flash中启动吗？是的，确实如此。现在要做的就是如何将uClinux的内核映像烧写到Flash中。用写入

器将uClinux内核映像写入到Flash中，然后将Flash焊接到pcb板上或插到开发板的flash的插座上可以吗？当然可以，

如果你有写入器的话。不过，很少有人手里能有这种写入器。我们需要的是一个廉价的Flash写入方

案。用JTAG，S3C4510B上集成了一个JTAG，通过JTAG我们可以控制S3C4510B上所有管脚，这样可以通过向JTAG接口输

入相应的指令和数据，用软件的方法在S3C4510B的数据、地址和控制总线上产生出Flash器件的读写操作时

序，将uClinux的内核映像文件烧写到Flash中（关于S3C4510B的JTAG接口电缆的制作和下载烧写uClinux映像文件

到Flash中的程序可执行文件和源代码请参阅http://www.dailzh.net上相关内容）。

终于将uClinux的映像文件烧写到Flash中了，用一根串口电缆将ARM7开发板和开发主机的Com1口连接起来，从网上

下载一个tip程序，执行这个命令：
tip –l /dev/ttyS0 –s 19200
等屏幕上显示 connected.以后，将ARM7开发板的电源接通。如果够幸运的话，你应该看到下面的信息：
>>Linux version 2.4.20-uc0 (root@dailzh) (gcc version 2.95.3
>>20010315 (release)(ColdFire patches - 20010318 from http://fiddes.net/coldfire/)
>>(uClinux XIP and shared lib patches from http://www.snapgear.com/)) 一 5月19 23:44:11 CST 2003
>>Processor: Samsung S3C4510B revision 6
>>Architecture: SNDS100
>>On node 0 totalpages: 4096
>>zone(0): 0 pages.
>>zone(1): 4096 pages.
>>zone(2): 0 pages.
>>Kernel command line: root=/dev/rom0
>>Calibrating delay loop... 49.76 BogoMIPS
…
>>Command: cat /etc/motd
>>Welcome to uClinux.org
>>For further information check: http://www.uclinux.org/
>>\>
uClinux在ARM7目标板上已经运行起来了，键入熟悉的ls命令，看有什么输出。

通过JTAG接口烧写uClinux映像文件到Flash中速度太慢，调试uClinux内核非常不方便，有没有其他的方法？有，

uClinux除了可以从Flash中直接运行外，还可以加载到内存中运行。我们来为ARM7开发板写一

个Bootloader，Bootloader的作用是初始化ARM7开发板，然后通过以太网接口将uClinux映像下载到内存中，然后从内

存中运行uClinux。这种方法下载uClinux内核映像只需要10几秒，适合于开发阶段经常修改uClinux内核时使

用(Bootloader for ARM7的源代码可以从http://www.dailzh.net 上下载得到)。调试完uClinux的内核以后，可以再

通过JTAG接口烧写uClinux映像文件到Flash中，这样又可以直接从Flash中运行调试好的uClinux了。

uClinux下用户程序的开发和调试
现在ARM7开发板可以运行uClinux了，如何开发uClinux下的应用程序呢？和在普通计算机上开发Linux程序一样，

首先编写应用程序的源代码，只不过编译的时候不能用gcc编译，需要用arm-elf-gcc编译。编译以后的可执行文件必须

下载到ARM7开发板上才能运行，下载程序到目标板上可以通过在uClinux中启用tftpd程序，在开发主机端用tftpcmd 程

序来下载，然后在uClinux的控制台窗口键入可执行文件名的方法来运行。如果想要应用程序在uClinux启动时自动运行

，可以修改uClinux系统中/etc/rc文件的内容来来实现。如果要用gdb来调试用户程序，可以www.uclinux.org上

的相关文档。

关于作者
dai lizhou , 爱好嵌入式系统和Linux开发, 如果你对嵌入式Linux开发有兴趣请访问我们的网

站http://www.dailzh.net, 诚邀你的加盟。
]]></description>
<pubDate>
2006-03-27 16:03:49.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308383.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308383.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[Arm-elf编译工具链的编译及Minigui]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308496.shtml</link>
<description>
<![CDATA[Arm-elf编译工具链的编译

一、环境说明

PC-CPU：i386

操作系统：Fedora II

操作系统自带GCC：version 3.3.3 20040412 (Red Hat Linux 3.3.3-7)

嵌入式CPU：ARM 920T

嵌入式内核：uClinux-Samsung-2510

嵌入式图形系统：Minigui-1.3.3

嵌入式应用程序：Mgdillo(浏览器)

二、重新编译原因

1．方案一采用20011219 版arm-elf编译器

购买的测试开发板自带uClinux-Samsung-2510内核以及其编译器版本为20011219的。但在编译Minigui时出现很多难以解决的编译错误（Minigui推荐的版本为20030314）。

       2．方案二采用20030314 版arm-elf编译器

若采用20030314版本来编译内核，能编译出来，代码却不能完全运行①，但可以拷贝20011219的elf2flt文件覆盖原来的文件，再编译内核就可以了。接着来编译Mgdillo，解决一些编译问题后，最后链接时出现一个无法修正的严重错误②。

由于上两种方案都不行，据网络信息说明可能要升级整个编译工具，因此就有下面系列工作了。

注：①BINFMT_FLT:reloc outside program ! init

②关于ld.real的段错误

三、编译前的源码准备

1．  下载工具链

进入http://www.uclinux.org/pub/uClinux/arm-elf-tools/tools-20030314/有如下文件，见备注栏说明

文件名
 备注
 
STLport-4.5.3.patch 
 需要
 
 STLport-4.5.3.tar.gz  
 需要
 
 binutils-2.10-full.patch  
 需要升级不下载
 
 binutils-2.10.tar.bz2  
 需要升级不下载
 
 build-uclinux-tools.sh  
 需要
 
 elf2flt-20030314.tar.gz  
 需要
 
 elf2flt-cygwin-020612.patch  
 不需要
 
 gcc-2.95.3-arm-mlib.patch  
 需要
 
 gcc-2.95.3-arm-pic.patch  
 需要
 
 gcc-2.95.3-arm-pic.patch2   
 需要
 
 gcc-2.95.3-cygwin-020611.patch   
 不需要
 
gcc-2.95.3-full.patch  
 需要
 
gcc-2.95.3-sigset.patch   
 需要
 
 gcc-2.95.3.tar.gz  
 需要
 
genromfs-0.5.1-cygwin-020605.patch   
 不需要
 
 genromfs-0.5.1.tar.gz  
 需要
 
 uClibc-0.9.19.patch.gz  
 需要升级不下载
 
 uClibc-20030314.tar.gz 
 需要升级不下载
 

2．  Binutils高版本下载

进入http://ftp.gnu.org/gnu/binutils 下载 binutils-2.11.2.tar.gz 文件

3．  uClibc高版本下载

进入http://www.uclibc.org/downloads/ 下载文件 uClibc-0.9.26.tar.gz和uClibc-locale-030818.tgz 。

4．  说明

所有下载的文件都先不解压缩放到自己一个目录中。

四、部分源码的升级说明

1．  Binutils的升级说明

把binutils-2.11.2.tar.gz 解压到当前目录会产生一个binutils-2.11.2目录，在文件build-uclinux-tools.sh中的一些操作都是争对binutils-2.10的，所以只要相对应的修改，并注释掉对binutils-2.10的升级语句就可以了。

2．  uClibc的升级修改

把uClibc-0.9.26.tar.gz解压到本目录，产生 uClibc-0.9.26的目录，文件名许改为uClibc 。

复制uClibc-locale-030818.tgz（无需解压）到uClibc\extra\locale目录中。因为Mgdillo的编译需要locale的支持。

修改uClibc/extra/Configs/config.in，在文件中有一段choice 和 endchoice包的代码，这结代码包函了Malloc的配置。删除整个段。采用下面代码

config MALLOC

bool "malloc"

default y

config MALLOC_SIMPLE

bool "malloc-simple"

default n

config MALLOC_STANDARD

bool "malloc-standard"

depends on UCLIBC_HAS_MMU

因为在整个编译过程中不允许出现编译配置确认的等待，必须全部配置完成。但choice的代码会不断的提示确认。

配置生成.config文件，在uClibc目录中运行

make oldconfig CROSS=arm-elf TARGET_ARCH=arm

就会出现配置操作过程，根据实际情况选择。配置完成后就在uClibc目录中产生一个.config文件，它包含了所以的配置。复制该文件到uClibc/extra/Configs/目录中并改名为Config.arm.default 。下面是我所配置的Config.arm.default的内容

#

# Automatically generated make config: don't edit

#

# TARGET_alpha is not set

TARGET_arm=y

# TARGET_cris is not set

# TARGET_e1 is not set

# TARGET_h8300 is not set

# TARGET_i386 is not set

# TARGET_i960 is not set

# TARGET_m68k is not set

# TARGET_microblaze is not set

# TARGET_mips is not set

# TARGET_powerpc is not set

# TARGET_sh is not set

# TARGET_sparc is not set

# TARGET_v850 is not set

 

#

# Target Architecture Features and Options

#

HAVE_ELF=y

TARGET_ARCH="arm"

# CONFIG_GENERIC_ARM is not set

# CONFIG_ARM610 is not set

# CONFIG_ARM710 is not set

# CONFIG_ARM720T is not set

CONFIG_ARM920T=y

# CONFIG_ARM922T is not set

# CONFIG_ARM926T is not set

# CONFIG_ARM_SA110 is not set

# CONFIG_ARM_SA1100 is not set

# CONFIG_ARM_XSCALE is not set

ARCH_LITTLE_ENDIAN=y

# ARCH_BIG_ENDIAN is not set

# ARCH_HAS_NO_MMU is not set

# UCLIBC_HAS_MMU is not set

UCLIBC_HAS_FLOATS=y

# HAS_FPU is not set

UCLIBC_HAS_SOFT_FLOAT=y

DO_C99_MATH=y

WARNINGS="-Wall"

KERNEL_SOURCE="/home/houen/arm-elf-toolschain/linux-2.4.x"

UCLIBC_UCLINUX_BROKEN_MUNMAP=y

EXCLUDE_BRK=y

C_SYMBOL_PREFIX=""

HAVE_DOT_CONFIG=y

 

#

# General Library Settings

#

# HAVE_NO_PIC is not set

# DOPIC is not set

# HAVE_NO_SHARED is not set

# ARCH_HAS_NO_LDSO is not set

# UCLIBC_PIE_SUPPORT is not set

UCLIBC_CTOR_DTOR=y

# UCLIBC_PROPOLICE is not set

# UCLIBC_PROFILING is not set

# HAS_NO_THREADS is not set

UCLIBC_HAS_THREADS=y

# PTHREADS_DEBUG_SUPPORT is not set

UCLIBC_HAS_LFS=y

MALLOC=y

MALLOC_GLIBC_COMPAT=y

UCLIBC_DYNAMIC_ATEXIT=y

# HAS_SHADOW is not set

# UNIX98PTY_ONLY is not set

# ASSUME_DEVPTS is not set

# UCLIBC_HAS_TM_EXTENSIONS is not set

# UCLIBC_HAS_TZ_CACHING is not set

# UCLIBC_HAS_TZ_FILE is not set

 

#

# Networking Support

#

# UCLIBC_HAS_IPV6 is not set

# UCLIBC_HAS_RPC is not set

 

#

# String and Stdio Support

#

UCLIBC_HAS_CTYPE_TABLES=y

UCLIBC_HAS_CTYPE_SIGNED=y

UCLIBC_HAS_CTYPE_UNSAFE=y

# UCLIBC_HAS_CTYPE_CHECKED is not set

# UCLIBC_HAS_CTYPE_ENFORCED is not set

UCLIBC_HAS_WCHAR=y

UCLIBC_HAS_LOCALE=y

UCLIBC_PREGENERATED_LOCALE_DATA=y

# UCLIBC_DOWNLOAD_PREGENERATED_LOCALE_DATA is not set

# UCLIBC_HAS_XLOCALE is not set

# UCLIBC_HAS_HEXADECIMAL_FLOATS is not set

# UCLIBC_HAS_GLIBC_DIGIT_GROUPING is not set

# UCLIBC_HAS_GLIBC_CUSTOM_PRINTF is not set

UCLIBC_PRINTF_SCANF_POSITIONAL_ARGS=9

# UCLIBC_HAS_SCANF_GLIBC_A_FLAG is not set

# UCLIBC_HAS_STDIO_BUFSIZ_NONE is not set

# UCLIBC_HAS_STDIO_BUFSIZ_256 is not set

# UCLIBC_HAS_STDIO_BUFSIZ_512 is not set

# UCLIBC_HAS_STDIO_BUFSIZ_1024 is not set

# UCLIBC_HAS_STDIO_BUFSIZ_2048 is not set

UCLIBC_HAS_STDIO_BUFSIZ_4096=y

# UCLIBC_HAS_STDIO_BUFSIZ_8192 is not set

UCLIBC_HAS_STDIO_BUILTIN_BUFFER_NONE=y

# UCLIBC_HAS_STDIO_BUILTIN_BUFFER_4 is not set

# UCLIBC_HAS_STDIO_BUILTIN_BUFFER_8 is not set

UCLIBC_HAS_STDIO_GETC_MACRO=y

UCLIBC_HAS_STDIO_PUTC_MACRO=y

UCLIBC_HAS_STDIO_AUTO_RW_TRANSITION=y

# UCLIBC_HAS_FOPEN_LARGEFILE_MODE is not set

# UCLIBC_HAS_FOPEN_EXCLUSIVE_MODE is not set

# UCLIBC_HAS_GLIBC_CUSTOM_STREAMS is not set

# UCLIBC_HAS_PRINTF_M_SPEC is not set

UCLIBC_HAS_ERRNO_MESSAGES=y

# UCLIBC_HAS_SYS_ERRLIST is not set

UCLIBC_HAS_SIGNUM_MESSAGES=y

# UCLIBC_HAS_SYS_SIGLIST is not set

# UCLIBC_HAS_GETTEXT_AWARENESS is not set

UCLIBC_HAS_GNU_GETOPT=y

 

#

# Big and Tall

#

UCLIBC_HAS_REGEX=y

# UCLIBC_HAS_WORDEXP is not set

# UCLIBC_HAS_FTW is not set

UCLIBC_HAS_GLOB=y

 

#

# Library Installation Options

#

RUNTIME_PREFIX="/usr/＄(TARGET_ARCH)-linux-uclibc/"

DEVEL_PREFIX="/usr/＄(TARGET_ARCH)-linux-uclibc/usr/"

 

#

# uClibc development/debugging options

#

# DODEBUG is not set

# DOASSERTS is not set

# UCLIBC_MALLOC_DEBUGGING is not set

# UCLIBC_MJN3_ONLY is not set

这样理论上在编译过程中就不会出现配置提示了，但不知什么原因还是出现了。但可以另外方法解决。

修改 build-uclinux-tools.sh 的fix_uclibc_config()函数。许改部分如下

              echo '# UCLIBC_HAS_MMU is not set'

              echo '# HAVE_SHARED is not set'

              echo '# BUILD_UCLIBC_LDSO is not set'

              echo '# HAS_SHADOW is not set'

              echo 'MALLOC=y'

              echo '# MALLOC_SIMPLE is not set'

              echo 'MALLOC_GLIBC_COMPAT=y'

              echo '# MALLOC_930716 is not set'

              echo '# UNIX98PTY_ONLY is not set'

              echo 'UCLIBC_CTOR_DTOR=y'

              echo 'UCLIBC_DYNAMIC_ATEXIT=y'

              echo '# UCLIBC_MALLOC_DEBUGGING is not set'

              echo "UCLIBC_HAS_WCHAR=y"

              echo "UCLIBC_HAS_LOCALE=y"

              echo "UCLIBC_HAS_THREADS=y"

              echo "# DOPIC is not set"

修改uClibc/libc/unistd/exec.c，这个文件不知道是不是一个 bug，在编译的时候该文件会被编译很多次，不同的预定义条件产生多个.o文件。

L_execl    -> execl.o

L_execv   -> execv.o

L_execle   -> execle.o

L_execlp  -> execlp.o

L_execvp ->execvp.o

在文件中还存在L___exec_alloc的定义，它包含的两个函数没有被编译进去，因而链接应用程序时会产生链接错误，没定义_exec_alloc 和__exec_free。根据实际情况我做了如下修改，把L___exec_alloc修改为 L_execl把_exec_alloc 和__exec_free 编译到execl.o中。

3．  其他修改

修改 build-uclinux-tools.sh 的multilib_table() 函数

注释片断如下几行

#echo "mbig-endian/fp

#echo "mbig-endian/fp

#echo "mbig-endian/ma

#echo "mbig-endian/fp

#echo "mbig-endian/fp

因为系统为小端的，uclibc的配置也是小端的。

              在build-uclinux-tools.sh文件开头，有  PREFIX=/usr/local设置安装路径。

五、编译及可能碰到的问题

1．正式编译

编译前还需解压elf2flt-20030314.tar.gz，生成一个elf2flt的目录。在当前目录中还需要产生一个链接指向嵌入式内核链接名为linux-2.4.x 。

在当前路径运行

./build-uclinux-tools.sh build 2>&1 | tee errs

就开始编译了

2．问题解决

       在build-uclinux-tools.sh文件末尾有一些 stage1 stage2的函数调用。你可以根据目录中产生的stage文件，查看进度。若出现stage1则表明函数stage1执行完成了。

       在编译过程中产生了问题，可以注释掉其他stage，仅仅调试错误的stage了。还有一个error文件是整个编译的过程，对除错有很大的帮助。

4．  编译内核的问题

这样编译出来的内核还是不能运行在开发办中。经过测试，还是需要内核自带编译器的elf2flt覆盖到 usr/local/arm-elf/bin中。

5．   

六、加入Minigui的编译环境

在Fedora中编译Minigui会产生一个错误，应该是汇编语句的错误。可以如下进行修改

在原来的文件中可能是这样书写的

(“

abcd

“    …..

       修改为

              (“abcd”

              空一行

       ……

假设路径就是/usr/local

CC=arm-elf-gcc AR=arm-elf-ar RANLIB=arm-elf-ranlib LDFLAGS="-elf2flt" ./configure \

--prefix="/usr/local/arm-elf" \

--host=arm-elf \

--build=i386-linux \

--disable-shared \

--enable-lite=yes \

--enable-video-fbcon=yes \

--enable-nativegal=yes \

--enable-nativeial=yes \

--enable-newgal=yes \

--enable-timerunitms=no \

--enable-nativegalqvfb=no \

--enable-fblin8=no \

--enable-fblin16=no \

--enable-fblin24=no \

--enable-fblin32=yes \

--enable-dummyial=yes \

--enable-qvfbial=no \

--enable-nativeps2=no \

--enable-nativeimps2=no \

--enable-nativems=no \

--enable-nativems3=no \

--enable-nativegpm=no \

--enable-textmode=no \

--enable-rbfsupport=yes \

--enable-rbfgb12=no \

--enable-vbfsupport=no \

--enable-fontsserif=no \

--enable-fontcourier=no \

--enable-fontsymbol=no \

--enable-fontvgas=no \

--enable-qpfsupport=no \

--enable-ttfsupport=no \

--enable-type1support=no \

--disable-ttfsupport \

--enable-latin2support=no \

--enable-latin3support=no \

--enable-latin4support=no \

--enable-latin9support=yes \

--enable-gbsupport=yes \

--enable-gbksupport=no \

--enable-big5support=no \

--enable-unicodesupport=no \

--enable-savebitmap=yes \

--enable-gifsupport=yes \

--enable-jpgsupport=no \

--enable-pngsupport=no \

--enable-imegb2312=no \

--enable-imegb2312py=no \

--enable-aboutdlg=yes \

--enable-savescreen=no \

--enable-tinyscreen=no \

--enable-extfullgif=no \

--enable-video-dummy=no \

--enable-extskin=no

make 

make install

七、Mgdillo的编译

./autogen

LIBS="-lminigui -lm -lpthread" CC=arm-elf-gcc AR=arm-elf-ar \

RANLIB=arm-elf-ranlib  \

LDFLAGS="-elf2flt " \

./configure \

--host=arm-elf \

--build=i386-linux  --enable-shared=no

make

可执行文件在src中
]]></description>
<pubDate>
2006-03-26 14:50:52.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308496.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308496.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[如何编写网络监视器]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308613.shtml</link>
<description>
<![CDATA[本文简单地介绍了NDIS (Network Driver Interface Specification 即网络驱动接口规范)，以及应用程序如何与一个驱动程序交互，如何最好地利用驱动程序。作为例子，本文提供了一个应用程序使用Packet.sys的网络协议层驱动程序的例子，读者在这个例子的基础上可以实现象Netxray等局域网数据包截获程序的功能。 
　　Packet.sys是DDK中的一个非常有用的驱动程序，通过它你能够接收以太网中所有经过你的电脑的数据包，并且可以脱离系统的TCP/IP协议栈独立发送数据包，即通过Packet.sys建立的与TCP/IP同层次的协议发送数据包。 

基础知识介绍 

1. 驱动程序 Driver 
设备驱动程序是拥有与Windows内核相同的最高特权的程序，它是在操作系统与输入/输出设备之间的一层必不可少的"胶水"。它的作用相当于转换器，将从操作系统发来的原始的请求转换成某种外围设备能够理解的命令。 
系统程序员的主要工作就是编写驱动程序，与系统的底层打交道。许多在应用程序中称为"mission impossible"即不可能完成的任务在使用了驱动程序后就可以轻易解决。编写驱动程序最主要的目的当然是为了驱动真正的硬件，使系统能够顺利地控制各种不同型号的外围设备或内部硬件，称为硬件驱动程序，象显卡驱动程序、网卡驱动程序、PCI总线驱动程序等等；还有的驱动程序是为了实现一些应用程序不能够完成的功能的，有的虽然在逻辑上实现了一个硬件的功能，但是物理上并不存在这个硬件，象虚拟光驱，这一大类则称为软件驱动程序，象TCP/IP驱动程序、防火墙的驱动程序、虚拟光驱的驱动程序等等。 
在Windows 9x/Me中支持Vxd驱动程序和WDM(Windows Driver Model)驱动程序，Windows NT中支持Kernel Driver即内核式驱动程序，Windows 2000及以后版本的Windows使用WDM驱动程序。Windows NT的内核式驱动程序与WDM驱动程序很相似，只是少了部分功能，而Vxd式驱动程序行将淘汰，所以我们这里用的是WDM驱动程序。 

2. 网络接口卡 Network Interface Card (NIC) 
网络接口卡俗称网卡，它是一种硬件设备，作用是在电脑的内部总线和网络的传输介质中充当大门的作用，通过它，我们可以向网络上发送和接收数据包。一般网卡的名称随着它所在网络的类型不同而不同，象处于以太网中的网卡叫做以太网卡，处于令牌网中的网卡叫做令牌网卡。我们这篇文章中讲的是在以太网中的应用。 

3. 网络驱动接口规范 Network Driver Interface Specification（NDIS） 
　　随着计算机网络蓬勃发展，网络相关的驱动程序成为驱动中的热点。为了提高编写网络驱动程序的效率，也为了使各种协议驱动在各种网卡之间独立，Microsoft创建了一个网络驱动程序界面规范，即Network Device Interface Specification (NDIS)，这个规范是为原本复杂的网络驱动程序编写框架提供一个并不严格的封装，在这个规范下，编写网络驱动程序中原来应该使用系统有关函数都变换为通过NDIS.sys这个接口，而内部实现的细节由NDIS.sys实现，这样，不仅提高了编写效率，程序员不易出错，而且也增强了驱动程序的强壮性、可维护性，设备独立性等性能。 
　　目前最新的NDIS是5.1版本，Windows 2K及以后版本的NDIS是5.0，我的例子中使用的是Windows 2K DDK。 
NDIS程序库(NDIS.sys)提供了一个面向NIC驱动程序的完全抽象的接口，如下图所示，网卡驱动程序与协议层驱动程序及操作系统通过这个接口进行通信。我们可以把这个接口看做Microsoft为网络驱动设计者提供了一个设计网络驱动程序所必须的抽象的伪"类"。（我个人认为，Microsoft引入NDIS是一个在C++的面向对象和C的高编译效率之间的一个折衷，就象MFC封装了WinAPI一样，以后Microsoft迟早会推出真正面向对象标准的DDK，就像现在DriverStudio等某些驱动编写工具所做的那样）NDIS库输出了所有的能够在NIC驱动开发中使用的NT内核模式函数。NDIS库还参与管理操作系统中的与网络有关的特定任务，管理所有底层的NIC驱动的绑定与状态信息。 



NDIS驱动程序有三种类型，分别是网络接口卡驱动程序、中间层驱动程序、高层协议驱动程序。 

A. 网络接口卡驱动程序 Miniport Network Interface Card drivers 

网络接口卡驱动程序管理网络接口卡，NIC驱动程序在它的下端直接控制网络接口卡硬件，在它的上端提供一个较高层的驱动能够使用的接口，这个接口一般完成以下的一些任务：初始化网卡，停止网卡，发送和接收数据包，设置网卡的操作参数等等。 
NIC驱动程序分为以下两种： 

l 无连接的微端口驱动程序 (Connectionless Miniport Drivers) 
无连接的微端口驱动程序是控制无连接的网络介质上的网卡的驱动程序，象以太网(Ethernet)、光纤分布式数据接口(FDDI)、令牌网(Token Ring)。 

l 面向连接的微端口驱动程序 (Connection-oriented Miniport Drivers) 
面向连接的微端口驱动程序是控制面向连接的网络介质上的网卡的驱动程序，象异步传输模式（ATM）。 

B. 中间层驱动程序 Intermediate Protocol Driver 

中间层驱动程序在协议驱动程序和微端口驱动程序之间。在高层的传输层驱动程序看来，中间层驱动程序象一个微端口驱动程序，而在底层的微端口驱动程序看来，它象一个协议驱动程序。使用中间层驱动程序的最主要的原因可能是在一个已经存在的传输层驱动程序和一个使用新的，传输层驱动程序并不认识的媒体格式的微端口驱动程序中相互转换格式，即充当翻译的角色。 

C. 高层的协议驱动程序 Upper Level Protocol Driver 

　　象各种TCP/IP协议，一个协议驱动程序完成TDI接口或者其他的应用程序可以识别的接口来为它的用户提供服务。这些驱动程序分配数据包，将用户发来的数据拷贝到数据包中，然后通过NDIS将数据包发送到低层的驱动程序，这个低层的驱动程序可能是中间层驱动程序，也可能是微端口驱动程序。当然，它在自己的下端也提供一个协议层接口，用来与低层驱动程序交互，其中最主要的功能就是接收由低层传来的数据包，这些通讯基本上都是由NDIS完成的。 

4. 驱动程序与应用程序的交互 
在windows NT/2K下编写的驱动程序都必须要包括一个名叫DriverEntry入口函数，这个函数是作为系统载入驱动程序时的入口点，它主要进行一些初始化及告诉系统各个回调函数的位置，系统只有通过DriverEntry函数才能够知道驱动程序中其他的函数。应用层的调用象CreateFile,ReadFile等等将导致NT输入/输出管理器生成一个与应用层的调用相对应的IRP(Input/Output Request Packet 输入/输出请求包)。在Windows NT下，几乎所有的输入/输出操作都是包驱动的，也就是每个I/O操作都是输出输入管理器向各个相关驱动程序发送IRP来实现的。IRP是一个数据结构，里面包含了完成这个I/O操作需要的各个参数和最终的状态等返回值。 

网络监视器例子原理 

　　好，基础知识都介绍完毕，下面我将讲解一个非常有用的例子，要应用这个例子，必须要有微软的驱动程序开发包，即DDK，我们使用的是开发包里的一个例子：协议层驱动程序Packet.sys，今天我们只解决如何在应用层使用这个驱动程序，以后再讲解Packet.sys的细节。Packet.sys是一个协议层驱动程序，它工作在OSI中的传输层，见下图，也就是说， 


　　如果你将它加载到系统中的话，你就拥有一个与TCP/IP、IPX等等协议层驱动程序同等级的协议层，这是一个令人兴奋的事情，至少我当初是这么想的：你可以脱离TCP/IP而自己发送接收数据包，你可以完成许多原来不能完成的事情，象截获局域网(我讲的是以太网，由于以太网的广播性质，所以网上的所有机器都可以获得网上数据包的复本)上的经过你的机器这个网络结点的所有的数据包，这个功能就是Netxray等网络监视软件的基本原理，如果你想做局域网访问限制器的话，只有根据接收的数据包稍加修改再发送出去就可以了。所以说，这个驱动的应用范围是非常广的，当然也是非常有用的。 

　　下面讲讲网卡的工作过程，下面所说的都是在一个局域网里的情况：当一个机器向网上发送出一个数据包的时候，网上的所有机器上的网卡都将接收到这个数据包，它将判断这个数据包的目的地是不是它，如果是的话就接纳，如果不是就丢弃。 
网卡有几个工作模式：广播模式、多播模式、直接模式和混杂模式。 
　　网卡在设置为广播模式时，它将会接收所有目的地址为广播地址的数据包，一般所有的网卡都会设置为这个模式。 
　　网卡在设置为多播模式时，当数据包的目的地址为多播地址，而且网卡地址是属于那个多播地址所代表的多播组时，网卡将接纳此数据包，即使一个网卡并不是一个多播组的成员，程序也可以将网卡设置为多播模式而接收那些多播的数据包。 
　　网卡在设置为直接模式时，只有当数据包的目的地址为网卡自己的地址时，网卡才接收它。 
　　网卡在设置为混杂模式时，它将接收所有经过的数据包，这个特性是我们要编写网络监视程序的关键。 

　　当然一般的应用程序是不能轻易设置网卡的工作模式的，不过我们借助Packet.sys驱动程序就可以将网卡设置为以上的任意模式。在我们的例子中，我们将网卡设置为混杂模式以接收所有的数据包。 
　　Packet.sys是NT DDK中的一个例子。这个驱动程序能够把网卡设置为我们需要的任意模式，允许应用程序通过它向网上发送和接收数据包。这个例子中还包括了一个方便使用驱动程序的DLL，Packete32.dll，它提供给应用程序一个方便的接口，而与驱动程序通讯相关的复杂的内部操作由DLL完成，面向应用层的程序员不需要了解这些细节。下面的图形描述了我们的程序是如何同网卡通信的。应用程序调用了Packet32.dll中的函数，函数接着调 


　　用了Packet.sys中与请求相对应的入口点，驱动程序使用了Ndis.sys中输出的函数来与网卡通信。 
在上面的过程中使用了以下的数据结构： 

typedef struct _ADAPTER 
{ 
HANDLE hFile; // 包含由CreateFile 函数返回的句柄 
TCHAR SymbolicLink[MAX_LINK_NAME_LENGTH]; 
// 驱动程序的符号链接SymbolicLink 
} ADAPTER, *LPADAPTER; 

typedef struct _PACKET 
{ 
HANDLE hEvent; // 一个用于Adapter对象的事件的句柄 
OVERLAPPED OverLapped; // 用于与驱动程序异步输入输出的Overlapped结构 
PVOID Buffer; // 发送或接收的数据包的缓冲区首指针 
UINT Length; // Buffer的实际长度 
} PACKET, *LPPACKET; 

typedef struct _CONTROL_BLOCK 
{ 
LPADAPTER hFile; // 网卡对象的指针 
HANDLE hEvent; // event 的句柄 
TCHAR AdapterName[64]; // 网卡的名称 
// 接收的数据包的缓冲区 
HANDLE hMem; 
LPBYTE lpMem; 
// 发送的数据包的缓冲区 
HGLOBAL hMem2; 
LPBYTE lpMem2; 
ULONG PacketLength; // 数据包的长度 
ULONG LastReadSize; // 最后一次读取的长度 
UINT BufferSize; // 缓冲区的长度 
} CONTROL_BLOCK, *PCONTROL_BLOCK; 

下面是在应用程序中调用DLL的关键代码。 

// 变量定义 
CONTROL_BLOCK cbAdapter; 
// 从注册表中得到网卡的名称 
ULONG NameLength=64; 
PacketGetAdapterNames( 
CbAdapter.AdapterName, 
&NameLength 
); 
CbAdapter.BufferSize=1514; // 以太网帧的最大长度 

// 分配并锁定内存用来作为发送或接受缓冲区 
CbAdapter.hMem=GlobalAlloc(GPTR, 1514); 
CbAdapter.lpMem=(LPBYTE)GlobalLock(CbAdapter.hMem); 
CbAdapter.hMem2=GlobalAlloc(GPTR,1514); 
CbAdapter.lpMem2=(LPBYTE)GlobalLock(CbAdapter.hMem2); 
// 打开网卡以接收发送数据包，这些代码调用DLL中的PacketOpenAdapter函数，这个 
// 函数调用CreateFile函数，这样就打开了网卡与我们的协议驱动程序的绑定，使我们 
// 可以在以后进行读写操作 
CbAdapter.hFile=(ADAPTER*)PacketOpenAdapter(CbAdapter.AdapterName); 
// OpenAdapter失败 
if (CbAdapter.hFile = = NULL) 
{ 
AfxMessageBox("Open Adapter failed"); 
} 
// 一个接收PacketAllocatePacket函数返回值的void 指针 
PVOID Packet; 
// 将网卡的工作模式设置为混杂模式 
Filter=NDIS_PACKET_TYPE_PROMISCUOUS; 
PacketSetFilter( 
CbAdapter.hFile, 
Filter 
); 
// 分配缓冲区用来接收数据包 
Packet=PacketAllocatePacket(CbAdapter.hFile); 
// 初始化接收缓冲区 
// 这些代码调用DLL中的PacketInitPacket来初始化Packet对象，这个Packet对象是用 
// 来保存接收网上的数据包 
if(Packet != NULL) 
{ 
PacketInitPacket( 
(PACKET *)Packet, 
(char *)pdData[nCurrentWriteLocation].pData, 
1514 
); 
// 从网卡驱动程序中读取数据包 
PacketReceivePacket( 
CbAdapter.hFile, 
(PACKET *)Packet, 
TRUE, 
&pdData[nCurrentWriteLocation].nPacketLength 
); 
} 

以上代码清楚地描述了应用程序如何使用Packet.sys驱动程序将网卡设置为混杂模式，这样我们就可以接收到经过我们电脑的所有数据包了。 

下面是一些我们在应用程序中主要用到的Packet32.dll中的函数。 

l PacketGetAdapterNames 
PacketGetAdapterNames函数从注册表中获得每个网卡的名称。 

ULONG 
PacketGetAdapterNames( 
PTSTR pStr, 
PULONG BufferSize 
) 
{ 
HKEY SystemKey; 
HKEY ControlSetKey; 
HKEY ServicesKey; 
HKEY NdisPerfKey; 
HKEY LinkageKey; 
LONG Status; 
DWORD RegType; 

// 依次打开键，并读取键值 
Status=RegOpenKeyEx( 
HKEY_LOCAL_MACHINE, 
TEXT("SYSTEM"), 
0, 
KEY_READ, 
&SystemKey 
); 
if (Status == ERROR_SUCCESS) { 
Status=RegOpenKeyEx( 
SystemKey, 
TEXT("CurrentControlSet"), 
0, 
KEY_READ, 
&ControlSetKey 
); 
if (Status == ERROR_SUCCESS) { 
Status=RegOpenKeyEx( 
ControlSetKey, 
TEXT("Services"), 
0, 
KEY_READ, 
&ServicesKey 
); 
if (Status == ERROR_SUCCESS) { 
Status=RegOpenKeyEx( 
ServicesKey, 
TEXT("Packet"), 
0, 
KEY_READ, 
&NdisPerfKey 
); 
if (Status == ERROR_SUCCESS) { 
Status=RegOpenKeyEx( 
NdisPerfKey, 
TEXT("Linkage"), 
0, 
KEY_READ, 
&LinkageKey 
); 
if (Status == ERROR_SUCCESS) { 
Status=RegQueryValueEx( 
LinkageKey, 
TEXT("Export"), 
NULL, 
&RegType, 
(LPBYTE)pStr, 
BufferSize 
); 
// 关闭上面所有打开的键 
RegCloseKey(LinkageKey); 
} 
RegCloseKey(NdisPerfKey); 
} 
RegCloseKey(ServicesKey); 
} 
RegCloseKey(ControlSetKey); 
} 
RegCloseKey(SystemKey); 
} 
return Status; 
} 

l PacketOpenAdapter 
下面的PacketOpenAdapter函数的流程： 

　　上面的流程是从应用程序的PacketOpenAdapter函数调用开始的，这也适用于所有其他的函数调用。函数PacketOpenAdapter为设备(Device)定义了一个新的DOS设备名(DOS Device Name，通过这个DOS设备名，我们应用层的程序才可以向驱动程序提出请求)，接着调用CreateFile函数来建立并打开一个联系设备的文件句柄。这个函数必须在我们进行其他操作比如读写数据包之前完成。CreateFile函数将进入驱动程序的IRP_MJ_CREATE入口点，在这里，它调用了NDIS库中输出的函数NdisOpenAdapter来完成操作。 

PVOID 
PacketOpenAdapter( 
LPTSTR AdapterName 
) 
{ 
LPADAPTER lpAdapter; 
BOOLEAN Result; 

ODS("Packet32: PacketOpenAdapter
"); 
// 为Adapter 对象分配全局内存 
lpAdapter=(LPADAPTER)GlobalAllocPtr( 
GMEM_MOVEABLE | GMEM_ZEROINIT, 
sizeof(ADAPTER) 
); 
if (lpAdapter==NULL) { 
ODS("Packet32: PacketOpenAdapter GlobalAlloc Failed
"); 
return NULL; 
} 
// 将名称拷贝到Symbolic link中 
wsprintf( 
lpAdapter->SymbolicLink, 
TEXT("\\.\%s%s"), 
DOSNAMEPREFIX, 
&AdapterName[8] 
); 
// 为设备定义一个DOS设备名 
Result=DefineDosDevice( 
DDD_RAW_TARGET_PATH, 
&lpAdapter->SymbolicLink[4], 
AdapterName 
); 
if (Result) 
{ 
// 创建一个设备的文件句柄（file handle） 
lpAdapter->hFile=CreateFile(lpAdapter->SymbolicLink, 
GENERIC_WRITE | GENERIC_READ, 
0, 
NULL, 
CREATE_ALWAYS, 
FILE_FLAG_OVERLAPPED, 
0 
); 
if (lpAdapter->hFile != INVALID_HANDLE_VALUE) { 
return lpAdapter; 
} 
} 
ODS("Packet32: PacketOpenAdapter Could not open adapter 
"); 
GlobalFreePtr( 
lpAdapter 
); 
return NULL; 
} 

l PacketAllocatePacket 
下面的函数PacketAllocatePacket为packet对象分配内存。 

PVOID 
PacketAllocatePacket( 
LPADAPTER AdapterObject 
) 
{ 
LPPACKET lpPacket; 
// 为Packet对象分配内存 
lpPacket=(LPPACKET)GlobalAllocPtr( 
GMEM_MOVEABLE | GMEM_ZEROINIT, 
sizeof(PACKET) 
); 
if (lpPacket==NULL) { 
ODS("Packet32: PacketAllocateSendPacket: GlobalAlloc Failed
"); 
return NULL; 
} 
// 建立一个事件，这个事件将在操作完成后激活 
lpPacket->OverLapped.hEvent=CreateEvent( 
NULL, 
FALSE, 
FALSE, 
NULL 
); 
if (lpPacket->OverLapped.hEvent==NULL) { 
ODS("Packet32: PacketAllocateSendPacket: CreateEvent Failed
"); 
GlobalFreePtr(lpPacket); 
return NULL; 
} 
return lpPacket; 
} 
l PacketInitPacket 
函数PacketInitPacket初始化packet对象，即将packet对象中的buffer设置为传递的buffer指针。 

VOID 
PacketInitPacket( 
LPPACKET lpPacket, 
PVOID Buffer, 
UINT Length 
) 
{ 
lpPacket->Buffer=Buffer; 
lpPacket->Length=Length; 
} 

l PacketReceivePacket 
函数PacketReceivePacket调用驱动程序的相应的入口点来从网络上读取一个数据包。这个操作是通过调用ReadFile函数来实现的。 

BOOLEAN 
PacketReceivePacket( 
LPADAPTER AdapterObject, 
LPPACKET lpPacket, 
BOOLEAN Sync, 
PULONG BytesReceived 
) 
{ 
BOOLEAN Result; 
// 设置偏移量(Offset)为0 
lpPacket->OverLapped.Offset=0; 
lpPacket->OverLapped.OffsetHigh=0; 
if (!ResetEvent(lpPacket->OverLapped.hEvent)) { 
return FALSE; 
} 
// 调用ReadFile 来读取一个数据包 
Result=ReadFile( 
AdapterObject->hFile, 
lpPacket->Buffer, 
lpPacket->Length, 
BytesReceived, 
&lpPacket->OverLapped 
); 
if (Sync) { 
// 调用者设定为未接收到数据包将等待，即同步调用 
// 所以我们使用Overlapped中的同步对象来等待数据包 
Result=GetOverlappedResult( 
AdapterObject->hFile, 
&lpPacket->OverLapped, 
BytesReceived, 
TRUE 
); 
} 
else 
{ 
// 如果调用者不想等待，则直接退出，他们会调用PacketWaitPacket来获得这次请 
// 求的最终结果 
Result = TRUE; 
} 
return Result; 
} 


具体应用 
当我们实现了网络监视器后，成功地从网络上截获数据之后，怎么办呢？在下篇中，我将讲如何具体应用。 


参考资料 

1． Windows NTDDK Help 
2． Windows NTDDK Packet.sys Sample 

首先，我声明一点，我们的网络监视功能是不能够阻止系统的一般协议栈对数据包的发送和接收的，它只是在比协议栈更低层次的地方(即网卡驱动程序的上端)获得了进入机器的数据包的一个"复本"，而"源本"则按照正常的流程向上传递给了相应的协议，"截获"这个词可能会让大家产生误解，我们的监视器只能实现"拷贝"这个功能，但说"拷贝"确实不太舒服，所以以后我还是使用"截获"这个词。 
　　当我们完成了一个能够成功地截获网上数据包的监视器了，但是这只是我们实现监视器的基础，还有许多工作要做，比如，当监视器截获数据包时，下一步应该干什么呢？什么事都不做当然不是目的。而且，我们不必要也不应该截获所有的数据包，我们要有目的的过滤那些需要的数据包，否则我们将看着海量的数据包而无所适从。 
下面我就从过滤数据包、增加功能和优化性能三个方面谈谈怎么在应用层实践网络监视。 
1．过滤数据包 

　　根据用户的需要过滤拷贝的数据包，提供给分析者分析，这可能是网络监视器的所能增加的最基本的功能了。要实现过滤，使用者必须要有网络的基本概念，特别要了解以太网帧的结构和IP，TCP/UDP等等数据包是如何封装在一个帧中的，这一节讲的是就是如何根据自己的需要识别各种数据包的结构并过滤它。 

　　为了在一个分层次的网络上传输数据，我们将数据从我们的应用程序传送到一个协议栈上，当数据在栈上一层一层地向下传送时，每一层的相应协议将把上一层传送下来的数据封装为自己的格式，举一个最普通的数据传递过程，即应用->TCP->IP->以太网流程，如下图所示： 

　　在图中我们可以清楚地看到TCP/IP协议栈及以太网中的数据传送的层次关系：当我们在应用程序（一般应用有HTTP、FTP等等协议）中将应用数据（包括用户数据和应用首部）向网络传送，它首先到达TCP层，TCP协议根据应用层的要求在TCP首部填写好各个字段，比如端口号、序号、标志等等，重要的一个步骤是填写数据校验和到校验和字段，然后将包括TCP首部的段（数据包在TCP协议层称为段segment）向协议栈的下一层即IP层传送；IP层则与TCP层一样，填写IP首部的各个字段，比如地址、协议类型等等，然后将在头部包括IP首部和TCP首部的整个数据报（数据包在IP协议层称为数据报datagram）向下传送；到了以太网驱动程序，他将继续进行封装工作，将以太网首部和以太网尾部添加到从IP层传下来的数据报上。 

下面我们从外向内看各个封装的格式。 

l 以太网帧首部 

　　以太网帧的首部的组成是：6字节的目的硬件地址、6字节的源硬件地址和2字节的类型字段。如下图所示。对于类型字段我们主要使用以下几种： 

协议 类型字段 
IP 0800h 
ARP 0806h 
RARP 0835h 


l IP首部 

IP的全称是Internet Protocol即网际协议，这个协议是TCP/IP协议族中的核心协议。下面是它的数据报格式： 

从图中可以看出，如果IP数据报没有选项的话，那么IP首部有20字节。对网络监视器来说，IP首部中各字段中重要的有：IP地址、协议类型、总长度。 
协议类型说明是什么协议(TCP,UDP,ICMP,IGMP)向它传递数据。下面是各个主要协议的代码： 
协议类型 协议代码（十进制） 
TCP 6 
UDP 17 
ICMP 1 
IGMP 2 
所以我们可以根据协议的代码来判断数据报内部封装的数据是属于什么协议。 

下面的四个协议(ICMP、IGMP、TCP、UDP)都是封装在IP数据报中的。 

l ICMP首部 

ICMP的全称是Internet Control Message Protocol即网间控制报文协议。著名的Ping程序用的就是这个协议。它的首部结构见下图。 


l IGMP首部 

IGMP的全称是Internet Group Manage Protocol即因特网组管理协议。它是用来支持主机和路由器进行多播的协议。 


l TCP首部 

TCP的全称是Transport Control Protocol即传输控制协议。它是非常重要的协议。我们的FTP，HTTP，TELNET等我们经常使用的应用都是使用TCP来传输的。它提供一种面向连接的、可靠的字节流服务。下面是它的首部的结构。 

如图所示，TCP首部长20字节，包括了源端口、目的端口、序号、确认序号、首部长度（以四字节记）、六个标志字段、窗口大小、校验和等等。 
六个标志字段的意义见下表： 

标志 意义 
URG 紧急指针(urgent pointer)有效 
ACK 确认序号有效 
PSH 接收方应该尽快将这个报文段交给应用层 
RST 重建连接 
SYN 同步序号用来发起一个连接 
FIN 发端完成发送任务 


l UDP首部 

UDP的全称是User Datagram Protocol即用户数据报协议，与TCP区别，它是面向无连接应用的协议，象我们的OICQ、ICQ等聊天软件都是用的UDP协议。下面是UDP首部的结构。 

我们可以看到，UDP首部比TCP首部要简单得多，这是因为UDP是无连接的，比TCP的有连接中双方复杂的交互要简单，它并不保证数据传输的质量，但是我们可以在更高层的应用中自己进行质量保证。 

l HTTP 

　　HTTP的全称是Hyper Text Transfer Protocol。我们浏览网页用的就是这个协议。HTTP数据是在TCP包中的，而且一般来说网页服务是在80或8080端口。所以如果你只需要分析浏览网页的情况，只需要截获端口号有80或8080的TCP包就可以了。更进一步，我们需要分析HTTP协议中请求的内容。 
　　我们可能需要得到用户浏览网页的URL地址。假设我们现在得到了包含在TCP包中的HTTP头信息。任何我们在浏览器里对HTTP服务器发送的请求都是GET或POST请求中的一种。浏览器在HTTP头里添加了与请求及系统相关的信息然后将请求发送给相应的服务器。那些信息当然包括了我们想要的URL。所以我们分析HTTP头就可以得到URL。下面是一个正常的包含于HTTP头中的URL： 
GET / HTTP/1.1 
　　上面的请求是得到服务器的主页（即默认页）的HTTP请求。"/"表示服务器的主页，后面的"HTTP/1.1"表示这是HTTP的1.1版本。这是现在的主流版本，也有1.0的老版本。 
下面是一个我们访问服务器上其他的网页的请求。 
GET /source/index.html HTTP/1.1 
上面的请求是得到"/source/index.html"的请求。 
从上面我们可以知道如何解析HTTP头。 

l 小结 
通过上面对数据格式的介绍，我们可以轻松灵活地进行数据包的过滤。 

2．增强我们的程序的功能 

　　我们当然不会满足于实现仅仅能够捕获数据包的简单的应用程序，要不是，我们干嘛这么辛苦编程呢，直接用Netxray等软件得了，我们的目的是能够让程序实现自己的功能。一个有意思的功能就是能够得到局域网上他人使用的代理服务器，实现方法很简单，别人使用代理服务器的连接特征一般是在服务器返回给用户端的HTTP头中有"Proxy-Connection"关键字存在。 
下面我从通过实现一个访问限制器讲讲怎么增强程序的功能。 
由于局域网的特殊性，我们可以实现一个局域网的访问限制器，可以限制网上其他用户对因特网的访问权限。要实现这样的功能，我们先讲讲如何通过协议驱动程序发送数据包。 
使用Packet32.dll中的函数PacketSendPacket来发送数据包。下面是这个函数的定义： 
BOOL 
PacketSendPacket( 
LPADAPTER AdapterObject, // 我们使用PacketOpenAdapter打开的网络适配卡对象 
LPPACKET lpPacket, // 使用PacketAllocatePacket等函数建立的数据包对象， 
BOOLEAN Sync // 是否同步 
); 

如果函数发送数据包成功，则返回True，否则返回False。 
　　下面我讲讲构建数据包中需要注意的地方。 
　　首先是字与双字在各种系统中内部存储的方式的不同，在Windows中字与双字是高位在低地址排列的，而网络传输的标准是低位在低地址排列，比如一个十进制数字4660在Windows系统中存储成3412h，而在网络上表示是1234h。所以我们在设置或读取协议首部中有关用字或双字表示（一般象TCP中的端口、序号，而IP地址则不是）的字段时要切记转换他们的排列顺序。下面是一个转换字排列顺序的转换算法： 

WORD SwapWord(WORD WordToReverse) 
{ 
WORD lo,hi; 
WORD result; 

lo= WordToReverse & 0xff; 
hi= WordToReverse & 0xff00; 
lo=lo<<8; 
hi=hi>>8; 
result=hi | lo; 

return result; 
} 

　　在我们建立发送包的过程中，除了设置包中IP首部、TCP首部中各种字段为我们需要的值，一个非常重要的工作是计算TCP、UDP、IP的校验和。我们遇到的校验和计算就是把一个范围的数据按字（16 bit，WORD，即两个字节）反码相加，如果数据不是字对齐的，则将在最后补上一个填充字节0使之字对齐再进行计算（在IP校验和计算中，由于只要计算IP首部，所以没可以出现这种情况，但是我们在后面的TCP、UDP校验和计算中碰到这种情况），以上计算得到的结果就是校验和。下面是一个公用校验和计算函数，它可以用在IP、TCP、UDP校验和的计算中： 

WORD CheckSum(WORD *addr,WORD len) 
{ 
DWORD lSum; 
WORD wOddByte; 
WORD ChecksumAnswer; 

lSum=0l; 

while(len>1) { 
lSum+= *addr++; 
len-=2; 
} 

if(len==1) { 
wOddByte=0; 
*((unsigned char*)&wOddByte)=*(unsigned char*)addr; 
lSum+=wOddByte; 
} 

lSum=(lSum>>16)+(lSum&0xffff); 
lSum+=(lSum>>16); 
ChecksumAnswer=(unsigned int)~lSum; 

return CheckSumAnswer; 
} 

　　IP首部的校验和只计算IP首部的数据，而UDP校验和是计算整个UDP首部和UDP数据。 
　　UDP的校验和是可选的，尽管UDP校验和的基本计算方法与上面描述的IP首部校验和计算方法相类似（16 bit 字的二进制反码和），但是它们之间存在不同的地方。首先，UDP数据报的长度可以为奇数字节，但是校验和算法是把若干个16 bit 字相加。如前所述，我们可以在最后增加填充字节0 ，这只是为了校验和的计算（也就是说，可能增加的填充字节不被传送）。其次，UDP数据报包含一个12字节长的伪首部，它是为了计算校验和而设置的。伪首部包含IP首部一些字段。由于UDP可以不计算校验和，所以规定如果发送端没有计算校验和的话，校验和字段将设置为0。UDP数据报中的伪首部格式如下图所示。 

　　TCP校验和的计算方法与UDP大致相同，只是TCP伪首部中的长度为16位TCP长度。而且TCP的校验和是必须计算的。 
　　另外在我们发送TCP报文段的时候要注意序号的顺序，不然发送的报文段将得不到对方的承认。 
　　知道了以上发送数据包的必要知识，我们现在可以使用发送数据包来进行应用了：比如，前面我提到了访问限制器，我们可以在截获了某用户的对以太网的非法访问后（可以通过对IP地址的检查或者对HTTP头中URL的检查来实现），根据截获的TCP包的序号重新构建一个伪装成从服务器发送的TCP包，其中TCP包中的RST标志设为1，将它发送到网上的话，他们建立的连接将被断开，所以就实现了阻止用户访问非法站点的功能。 
　　从上面的应用我们也看到了局域网的不安全性，每个结点都可以得到任何结点的网络信息，甚至可以轻松地阻断别人的网络连接，那种方法如果用在黑客手里，你也不能有如何防御的手段，幸好这只是在局域网上，如果有人能够在路由器上获得截获，那将是不幸的事情。 

3．优化应用程序的性能 

　　用过Netxray等网络监视器的读者都知道，如果在一个繁忙的网络上进行截获，而且不设置任何过滤，那得到的数据包是非常多的，可能在一秒钟内得到上千的数据包。如果应用程序不进行必要的性能优化，那么将会大量的丢失数据包，下面就是我对性能的一个优化方案。 
　　这个方案使用了多线程来处理数据包。在程序中建立一个公共的数据包缓冲池，这个缓冲池是一个LILO的队列。程序中使用三个线程进行操作：一个线程只进行捕获操作，它将从驱动程序获得的数据包添加到数据包队列的头部；另一个线程只进行过滤操作，它检查新到的队尾的数据包，检查其是否满足过滤条件，如果不满足则将其删除出队列；最后一个线程进行数据包处理操作，象根据接收的数据包发送新数据包这样的工作都由它来进行。上面三个线程中，考虑尽可能少丢失数据包的条件，应该是进行捕获操作的线程的优先级最高，当然具体问题具体分析，看应用的侧重点是什么了。 ]]></description>
<pubDate>
2006-03-17 10:21:19.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308613.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308613.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[基于arm+uClinux的嵌入式系统的开发]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308691.shtml</link>
<description>
<![CDATA[前些日子基于arm＋uClinux开发了一个网络监控系统，眼看项目马上要做完了，终于松了一口气，于是整理了一些笔记和心得想和大家针对这种开发模式进行一些探讨，希望对各位有所帮助。
按照我的开发过程想分以下几部分逐一介绍。
1．开发平台的选择和论证
2．开发环境的建立
3．一般程序的开发
4．Linux程序向ARM+uClinux平台的移植
5．剩下的问题
希望诸位多多补充自己的想法，以利于大家共同提高。


1． 开发平台的选择和论证
一个项目拿到手，如何选择开发平台（主要是指CPU和操作系统以及开发环境和工具）应该说至关重要，有时这不光影响进度，产品质量，可维护性等一般问题，甚至涉及到方案的可实现性。本人结合自己的网络监控系统简单归纳了一些对平台的考虑，还请各位补充。

从系统功能实现考虑：
(1) 是否有片上外设，专用指令或配套的软件模块直接实现系统功能要求。 感 觉这一条对很多人的决策影响很大
(2) 价格
这一点应通过CPU提供的资源综合考虑，它提供了多少有用的资源，多少没用的资源（那可都是银子呀！），还是那三个字，性价比，另一方面，是要抓主要矛盾，是不是有些特性是必须的，什么特性是用户需求里的亮点（就靠这些亮点往上抬价），这时该花的就得花了。
(3) 功耗
本系统对CPU功耗要求不高，但对移动设备，这一点可是致命，而且这一点不是仅针对CPU，所有几乎器件都要勒紧裤腰带运行。
(4) 处理速度
这项不用多说，大家都明白重要性，但具体算起来可是一门学问，一方面是自己需要多快的速度，如果加上非实时操作系统这事就不好控制，余量还是大点稳妥，另一方面，CPU指令周期多少，有没有流水，有没有并行，什么体系结构，有没有专用指令（看人家DSP多牛，干这事一绝），对外部存储器和外设的存取速度等等，哪一个慢了都叫瓶颈。
(5) 需要的硬件支持（如外部存储器，双电源等）
这算是杂项，但会增加额外的价格，系统体积等，不容忽视。

从开发者的角度考虑：
(1) 是否有足够的技术支持包括demo版及原理图，demo程序，操作系统和BSP，测试开发工具等。
(2) 自身条件;包括对项目开发周期的要求，开发人员对器件和开发模式的熟悉程度以及掌握的难易程度。
(3) 可用资源是否丰富（书籍，网络等）
以上三点主要考虑迅速开发出稳定的系统。
(4) 系统的可继承性，可移植性和可扩展性。
(5) 是否有现货。
(6) 方案提供商的素质。（包括技术水平和服务意识）。

根据以上考虑选择了s3c4510b（ARM7TDMI）＋uClinux开发模式

(1) 以下是该平台对我的系统的满足情况：（和上面几点对应）
本监控系统硬件部分主要要求以下部分：
a．以太网接口 （s3c4510b自带网络控制器）
b．串口 （自带）
c．与数据采集芯片的接口（8位数据线，小于8位地址总线）。（自带）
本系统软件部分主要要求以下部分：
a． 硬件接口驱动程序 （uClinux提供串口和网络控制器驱动）
b． 网络协议栈支持（uClinux提供TCPIP，UDP等的协议栈）
c． 应用层程序（如果算上可以从linux移植的程序来看，那就太多了，我就用到了一个现成的）
(2) 本应用系统不是那种批量的东西，对价格要求不苛刻，而且这款CPU最便宜可以到55左右,可以接受。
(3) 本应用系统有固定电源，功耗要求不高。当然，据说ARM在节省功耗上很有特点。
(4) 本应用系统速度方面要满足两方面：1。串口：115200bps 2。网络速度 能到10Mbps就行，所以对系统速度要求也不高。这款ARM内部可以到50M。
(5) 系统对体积要求也不高，加片flash和RAM还是没问题（到目前为止感觉我的系统真是无欲无求！）

从开发者的角度考虑：
(1) 因为时间很紧（一个半月），所以支持越多越好。目前从开发商那里拿到了开发板，原理图，uClinux，相应驱动，bootloader，拿来就可以用了。软件硬件并行开发。（bootloader和网络控制器驱动没提供原码，比较可惜：－（

(2) 当时我对嵌入式系统的开发模式和ARM都是只有耳闻，linux接触过一个月左右。现在想起来有些后怕。
(3) 网上的资源，非常多。
提供一些我常用的。
http://www.uclinux.org/ uClinux的大本营。
http://www.ucdot.org/ 里面有些技术文章非常不错。
http://www.linuxdevices.com/
http://www.linuxeden.com/ 这是国产的linux站点。
uclinux-dev@uclinux.org 这是uClinux的邮件列表，回答问题的都是大牛，非常有帮助，记住把你的邮件设置成纯文本格式。 申请是在：
http://mailman.uclinux.org/mailman/listinfo/uclinux-dev web方式。

(4) 采用以上开发模式，软件的可维护性，可移植性和可扩展性都不错。
(5) 目前该CPU使用还是比较普遍，现货没问题。
(6) 方案提供商的素质吗……..还算可以吧:-)

根据以上考虑和目前的开发情况，这套方案还是比较令人满意。

今天先回家了，下回介绍具体开发步鄹吧。

2．开发环境的建立。
先说两句废话为和我以前一样对操作系统（尤其是嵌入式操作系统）迷惑的弟兄解释些概念。因为总是有人在问是不是一定要用操作系统，我的CPU能不能移植操作系统，可以移植什么操作系统，有了操作系统可不可以运行某些程序。
从我的个人经历来讲，这其实就是许多硬件出身的弟兄对操作系统这个东西有神秘感（和我一年前一样）。说白了，操作系统就是一段设计非常巧妙的程序，和你自己的程序从本质讲没有区别，于是，以上问题转为，我是不是一定要用这段程序，我的CPU能不能运行这段程序，可以跑什么样的程序。这个程序可以跑，调用这个程序接口的另一个程序能不能跑！
答案也就变得简单，操作系统对任何一个CPU都不是必须的（对嵌入式系统更是如此），你可以自己编些程序在没有操作系统的PC裸机上跑（BIOS就是这样的），像玩C51一样，（虽然奢侈的让人有些心痛），或者移植UCOS到上面。另一方面，现代操作系统大多需要一些硬件的支持，（像保护模式的实现），反过来说，高端CPU中专门有针对支持操作系统的体系结构，这样，许多操作系统的实现是挑剔硬件平台的。其实其它程序也一样，你编的程序使用的片上外设另一CPU上没有，那这段程序就无法移植了。这就是话粗理不粗。书归正传，还是聊聊ARM＋uClinux开发模式下开发环境的建立（其实下面说到的东西不仅限于这种硬件平台和操作系统）

很久以前就在介绍嵌入式系统开发的书上见过“交叉编译环境”这词，当时觉得很玄，用了以后才知道，其实就是解决在谁的地盘上用谁的工具编谁的代码问题。
编译的最主要的工作就在将你的程序转化成运行该程序的CPU所能识别的机器代码，不同的CPU有相应的编译器，另一方面。编译器本身也是程序，当然也要在某一个CPU平台上运行。于是交叉编译的交叉点就在那个编译器本身是CPU1上的一个程序，却在为CPU2编译代码（整个一个吃里扒外！）。这么一想，以前用51和dsp的开发软件（大部分都是IDE－集成开发环境）开发程序时，都算是交叉编译啦。当然，假如在你的ARM系统上，操作系统已经正常运行，并且你的资源足够多，你可以把PC机上运行的ARM编译工具移植到ARM上，然后所有该系统的应用程序都直接在ARM系统上编译，这就不算交叉编译，但如果有条件这么作，程序的开发或者移植就方便多了，因为整个开发过程又回到在自己PC机上编应用程序的那种模式了，那就是在自己的地盘上用自己的编译器编自己的应用程序。
与不使用操作系统的开发模式不同（此处的操作系统尤其指提供了专门的接口函数库的操作系统，目前的UCOS就不算），在目标板（就是实现系统的板子）使用操作系统的开发模式下，交叉编译环境中还需要该对应该操作系统的库。比如uClinux提供的uClibc。此时，开发用的主机上不光要有目标板CPU所需的编译工具，还要有对应操作系统的库，又因为一般库文件还要在开发机上拿目标CPU的编译器重新编译一下，所以还要把操作系统的原码也放到开发机上。（唉，跟目标板没什么关系，却要帮它背这么多东西，真是上辈子欠它的！！）。
虽然操作系统的接口库至关重要，但大家似乎已经淡忘了它的存在。这些多是因为大家已经远离了刀耕火种的年代（需要告诉编译器需要的include路径，lib路径，以及lib的名称），集成的编译环境让我们编译链接的所有繁琐工作化作对BUILD按钮的潇洒一击。而且不论是windows环境，还是linux环境，都有环境变量去记录这些参数。。但尝试将/usr/lib目录改一个名字，你就会知道你不能无视他们的存在，因为操作系统的功能都是通过这些库来交给应用层程序使用的。当然如果你的系统不依靠任何操作系统，像最原始的那种完全自己实现所有代码，就只需要一个编译工具，少了这些罗嗦事。
以上的东西一般时候是没有必要仔细研究，但交叉环境下开发或移植比较大的程序时，你可能就需要了解编译器，链接器等开发工具的几乎所有重要参数。
我在开发时，主机完全使用的是linux，如果有条件，建议大家这样作，linux的使用没有想象的复杂（虽然我现在身边还要放一本关于linux使用的书籍），而且开发程序可以先在主机上调通，然后用交叉编译工具为目标系统重新编译一遍，可以这样做是因为主机是linux，目标系统跑uClinux，两个操作系统提供的应用程序接口几乎是一样的，所以程序几乎不用修改。
在我的系统上，建立基本的开发环境过程如下。
(1) 安装gnu开发工具链（是GNU开发的针对ARM CPU的一组编译开发程序（是linux程序）。包括arm－elf－gcc，arm－elf－ld等
(2) 将uClinux源代码源代码解压到相应路径下，按照编译内核的步鄹编译一遍（此时使用的编译工具已经是上面提到的ARM编译工具了，因为它要在ARM CPU上运行，另外，和编译linux内核一样，此时可以通过menuconfig来对内核提供的功能进行裁减
(3) 将库（uClibc）解压到相应路径下，用以上工具编译一遍。
这样最基本的环境就算搭建好了。

以上工作对于做过的人来说比较简单，这里介绍一下帮助没有使用或刚开始使用这种开发模式的弟兄们理清一下思路。

 
3．应用程序的开发
因为目标板上用uClinux，它提供的程序接口和linux下的基本一致，不一致的部分主要在于uClinux不支持MMU（应该说是uClinux是为不带MMU的cpu定制的），最明显的就是fork函数要用vfork函数替代，这也是编程时，感觉最不爽的一点(没办法，谁让咱们的CPU有生理缺陷)。另一个不易觉察的差异在于uClinux提供的库uClibc是经过裁减的。更适合于资源紧张的嵌入式系统（上回分解已经说了，应用程序很大一部分是在和库函数打交道，而且大家最终是链在一起，所以库函数大了，你的程序也小不了）。
于是基于这种开发模式的应用程序开发变成了linux下的程序开发。而且在实际中一般是编好了程序先在主机上拿主机平台上的编译器编译并且调试一下（linux下的编译器就是gcc了），当然前提是被调试的程序中需要的硬件条件主机具备，例如我的程序中有一段是针对串口的，于是先在主机编一个串口程序，调通以后拿目标板的编译器重新编译一下（如果看了上一章“交叉编译环境”，这里就不会晕了），下载到目标板上运行，一般来说就可以直接用了。
以上也是为什么我认为开发嵌入式linux程序主机应该选用linux环境。对于以前没用过linux的人来说（比如我），开发程序前应该花3，4天时间熟悉linux环境，尤其是它的编辑器，用惯集成编译环境的人有时连编译器和编辑器的概念都模糊了，所以一般是直接进入集成编译环境，连写带编一气呵成，殊不知有些集成编译器提供的编辑器弱智的一塌胡涂，如果用熟了linux下的emacs，你就会发现他们之间的差距大概……要像我和盖茨那么大吧。所以编程序时应该选一款优秀的编辑器，linux下，我当然选emacs，虽然刚看见它的感觉是外表丑陋，使用复杂。但只要多用多练，对提高效率很有帮助。（将你的程序用两个编辑器完成，一半是用emacs的，一半是不用emacs的，看看效果：－）
对具体的linux编程我就不板门弄斧了，需要提个醒的是咱硬件出身的人作软件应该养成良好的编程习惯，别让作软件的笑话咱。因为作了些网络应用，所以介绍一些网络编程时要用到的网站和书籍;
<<unix 网络编程 第一卷>>w.Richard.Stevens. 这可是linux网络编程的圣经级的书籍
http://www.fanqiang.com/a4/b7/ 适合于网络编程的入门。
还有IBM中国上关于linux的教程和文章，都是翻译过来的，有很多写非常不错。
其实类似的资源不计其数，遇到问题时应该先到google上狂搜一圈。
重点想说些关于编译器的东西，不了解它，在交叉编译环境下编译程序就寸步难行了，这无非是因为交叉编译环境下目标板编译器所处的寄人篱下的悲惨环境。想想在linux下将myprogram.c编译链接成应用程序myprogram，最简单的一句gcc –o myprogram myprogram.c 就可以了。（其实在诸如VC下你也可以找到类似的命令，集成开发环境只不过替你来调用它了）。一切看起来天经地义。
但试着把/usr/include路径改一个名字（比如改成stupid_include），再这样编译一下，会发现程序中被< >引用的头文件（比如#include<stdio.h>）都找不到了。因为编译器看见这样的头文件会到系统指定的路径下寻找，而这个路径是由环境变量保存的（linux和windows下都是这样的）。针对以上情况，不将路径名字改回去，但是给编译器加一个参数如下：
gcc –I/usr/stupid_include –o myprogram myprogram.c 会发现错误信息没了，一切又恢复了往日的宁静，顿时明白，不用环境变量，通过参数，同样可以将这些信息告诉编译器。 返回来说说你的目标编译器，虽然占用了人家的地盘，编译器，头文件，库文件，一个都不少，但你要编一个程序编译器照样发晕，因为没有环境变量告诉它自己需要的头文件和库文件在哪里。看来只有两种办法，一个是抢占了主机的环境变量改成自己的（整个儿一个土匪）,或者在编译时加上必要参数（还是这样绅士一些），告诉编译器需要的文件的位置。（除此之外，还有其他一些参数也是如此）。
从源程序到可执行文件根据情况不同可能分好几步，一般每一步可能都会有一个应用程序实现，像gnu提供的arm开发工具链其实就是这么一组程序。提供从编译到链接到格式转化的全套服务。你可以用arm－elf－gcc命令一步到底直接产生可执行文件（其实也是在自己的任务完成后调用下一个程序），也可以每一步加上自己的参数，只作自己的事。
编译器的主要参数的使用下次将程序的移植时再讲。这里想说一下编译器产生应用程序的几个主要步鄹，讲这个问题的原因还是很多人无法区分诸如编译和链接，不用问，这一切还是IDE集成开发环境惹的祸。有人会说，IDE招你惹你了，你老贬它。其实不然，首先以上说的东西一般在IDE的project菜单下的option或build option中找到，只是一般不用管罢了。另一个方面，IDE就像是傻瓜照相机，很多工作他都帮你完成了，使用简单。但如果要做摄影师的话，你就少不了要对每一个细节都了解。其实编译程序也是一样。（你可以对优化，警告级，宏定义等诸多选项进行自己的选择）。以下是几个主要步鄹：（以下有些我也不确认，如发现问题，请及时纠正。
(1) 预编译。 主要工作就是处理所有＃开头的，包括头文件。以前搞不清头文件和可执行文件有没有什么联系（因为总看见两个文件名字取一样的），现在知道，他们之间没有任何联系。在预编译结束后，头文件的使命就结束了。在下一次介绍不同平台程序移植时可以看到，预编译有时非常有用。
(2) 编译。编译应该是最主要的一步，就是将源文件生成CPU能识别的语言，一般是 后缀为.o的目标文件，应该说，此时的文件就已经可以执行了。当然这个时候外部函数等外部符号都没有引入，对于被编译程序来说，这些外部符号还只是留一个倩影，压根儿不知它在不在。你可以在你的程序里调用一个不存在的函数，甚至都不用声明，在编译阶段，很多编译器只是给个警告。只有在链接时才会报错。（呵呵，够弱智！）
(3) 链接：链接才是清帐的时候，以前在程序里用到的外部符号都要把真正的东西交出来。你可以指定需要连接在一起的目标文件，也可以告诉编译器库文件的名字和路径（指定方法下次讲）。编译器会去找，需要注意的是，库的指定需要注意顺序。首先，如果不同的库里有同名函数，并且该函数被调用，那么在前面的就被链接进去了，这一点对于头文件路径的指定也适用，如果你自己的头文件和系统头文件相同，并且你的头文件路径在系统头文件路径前面，你的头文件就会代替头文件。库文件是由相应的程序（linux下是ar命令）将需要被添加到库里的目标文件（该文件是编译阶段生成的）组织起来生成档案文件，同时可以建立一个检索，检索内容为所包含的目标文件中定义的符号。也就是说，库文件并不是必须的，但它为经常使用的目标文件中的函数提供了快速的检索机制。
以上就是主要的步鄹，当然除此之外，还有一些用于格式转换的工具等。不一一介绍。知道编译器的细节对于程序的开发和移植都是很有好处的。

程序开发过程中调试也是至关重要，因为可以先在主机上调试，所以可以使用linux下的gdb，（有点像dos 下的debug）。但是只是用到了皮毛，还有一个专用于宿主机模式的调试工具gdbserver，一直没时间研究，希望用过的大侠多发些文章铺路。
另外还会遇到如何作ramdisk，如何让系统启动自己的程序，这些都太linux了，没接触过linux的人会晕，为了大家的健康，就不讲了，遇到问题可以给我email，大家一起讨论。

4.不同平台间程序的移植--简单程序的移植

研究程序移植的那两周是最痛苦的两周，没有太多可以借鉴的东西，只能摸黑向前走，于是更加坚定决心要整理些东西给后来的弟兄。不过话说回来，各位弟兄别被我前面说的吓倒，只要搞清你要作什么，程序移植其实是比较简单的事情。
首先列出一些问题：
(1) X86上运行的程序能不能在51单片机上运行，为什么，有没有可能，如果可以，应该做哪些工作才可以实现。
(2) 相同CPU平台，DOS的程序为什么可以在windows下运行，能不能在linux下运行，为什么，作什么工作可能实现。
为什么可以移植程序，为什么要移植程序?
程序可以移植首先要感谢开发出高级语言的大牛们，记住，无论多么漂亮的代码经过编译以后都要变成CPU可以识别的机器语言，而几乎一千种CPU说着一千种语言。为保证大家有共同语言，规定一种高级语言――高级语言。每一个CPU派出自己的翻译――编译器。这个翻译精通两国语言，高级语言和自己的语言。（由此已经可以看出编译工具在程序移植中的重要性）。只要程序没有硬件上的约束，可以说这种沟通是无极限的，甚至于不同操作系统平台。（操作系统也是程序，也可以移植喽）举例：在x86的windows下用VC（或TC，BC）编一个c程序实现i=i+1，丝毫不改就可以用51的C编译器重新编译并在51单片机上运行。一次小型的移植就结束了。

可以移植已有的程序还要感谢开放源代码的弟兄，没有这些C文件和H文件让你重新编译一下，怎么在你的CPU上跑？其实不止这些，后面还会看见开源组织的牛人专为程序可移植性所作的专门的工作。
那么为什么要移植程序？
问这问题就像问地上有个钱包为什么要捡一样，答案不言而喻。现成的东西为什么不要。当然，移植程序可没有捡钱包那么简单，尤其是第一次，后面会说一些移植之前应该考虑的问题。（就像现在地上有个钱包也千万别上去就揣自己兜里，说不定就是套）。另一方面，你给我你写好的程序让我拿去用，我还要考虑一下，或许里头问题多的还不如自己写一个。我这里所说的可移植的程序应该是维护比较好，比较成熟的源代码（像我后面的所说的UCD－SNMP），目前的开放源代码运动决不仅仅是把自己的程序公开就行了，而是有了一套成熟完整的版本控制，BUG报告和PATCH提交流程。
这样的代码才有更大的使用价值。
什么时候可以考虑移植程序？ 在基于嵌入式操作系统进行开发时，具有一定规模的程序都可以到网上查一查都没有成熟的源代码可用。前面已经说到，程序的移植最终只针对CPU，其实和操作系统没什么关系，但另一方面，因为代码中可能会使用一些库函数，这些库会包括C语言标准库和操作系统提供的API（应用程序接口）库。假设源代码中只包括C标准库，那么该程序就可以跨操作系统去移植。例如hello world程序中使用了printf，因为该函数是C标准函数，所以在X86上使用TC（BC或VC）可以直接编译运行，在ARM＋uClinux平台下也一样，但如果程序中调用了vfork函数，那么只有linux一脉相承的操作系统支持这种特殊服务了，在window或dos操作系统下无法直接编译该程序了。只能找该操作系统支持的类似的功能来实现。再进一步，硬件上的生理缺陷也会为移植带来麻烦，S3C4510B不支持MMU，在其上运行的uClinux也不提供和MMU有关的服务（其实uClinux本身可以支持MMU），于是在移植前相关的函数（比如FORK）都要被替代掉（使用VFORK）。好在uClinux和linux提供的应用接口大部分还是相同的。所以这样的工作还可以承受。
由上可知，如果是在各种嵌入式linux（除了uClinux以外，还有好几种）平台上开发，那么针对该平台以及linux平台上的源代码都可以使用，但是要牢记他们之间的差异。在我的系统中需要实现网络监控，所以想使用snmp协议，该协议和http，ftp一样属于应用层的成熟协议，专用于网络管理。目前已经有一些针对该协议成熟的代码，最有名的是ucd－snmp，不光软件本身功能强大，可移植性也比较好，在linux，unix等平台上都可以移植，于是决定将它移植到ARM＋uClinux平台上（别看现在说的这么轻松，当时接这活时都有点哆嗦）。
简单总结一下，移植应用程序的前提是有源代码，移植的关键工具是编译器，源代码中和硬件平台相关的东西越少越好（这里主要指使用了汇编，或做了针对自己平台的事，比如将指针指向特定地址然后操作），另一方面，如果该程序是基于某个操作系统（利用了操作系统提供的特殊服务，即API），要看自己的操作系统是否提供了相关服务。
下面简单列出一些我认为移植时需要考虑的问题：
(1) 自己的操作系统的特点以及在当前版本下支持的特性。
例如：uClinux不支持MMU，同样就无法支持相应的特性。
(2) 硬件资源。
因为嵌入式系统资源比较紧张，硬件资源考虑必须要周全：
(1) 软件存储空间的大小
这一般要等用目标编译器重新编译完以后可能才会知道，所以只能大概估算，但千万不要看这个程序在linux下只有几十k，就认为程序很小，这是因为linux下程序多时使用动态库，而在嵌入式系统中，很有可能是把用到的库都链接在一起，所以程序的尺寸会大大增加。
(2) 程序运行空间。.
(3) 硬件以及相应的驱动是否完备
以上工作应该尽量做，但有时事先无法把握，只能听天由命了（有没有搞错！！）

可能有人已经要晕菜了，振奋一下大家，如果找到了好的源代码（可移植性好），那么剩下的如要工作就是玩转你的编译器，只要你能顺利的把源代码用你的编译器重新编译一下。90％的工作就完成了（不是吗）
上回已经介绍了一些编译器方面的东西，下面针对我的ARM编译器的具体参数来讲解一些编译器主要参数的设置。
加入我已经有了hello.c,在x86的linux平台下编译链接一下。
gcc –c hello.c 产生.o
gcc –o hello hello.o 产生可执行文件，上回说过，主机编译器参数都有环境变量保存，所以看起来很简单。这里我故意分两个步鄹。
下面看一下用我的编译器编这个程序（心脏不好的先吃药）。
arm-elf-gcc -Iroot/uClibc/include -msoft-float -mcpu=arm7tdmi -fomit-frame-pointer -fsigned-char -mcpu=arm7tdmi -Os –Wall -DEMBED -D_uclinux_ -c hello.c
这只是编译，将参数逐一讲解。

Arm-elf-gcc 是gnu的arm编译工具
1）Include地址：参数：-I 值:root/uClibc/include（这是在主机上我的uClinux的头文件路径） 用法：-I root/uClibc/include

-I参数保证后面的头文件路径在搜索系统头文件路径前被搜索从而有可能替代系统的头文件，如果有多个这样的参数，则搜索的顺序是从左到右，然后是系统的头文件。
2）-m 是针对CPU的选项。
-mcpu=arm7tdmi 说明CPU类型
-msoft-float 产生包含浮点库的输出
-fsigned-char 让char类型有符号
-fomit-frame-pointer 对所有不需要帧指针的函数都不将其保存在寄存器中。
3） -Os –Wall
－Wall：所有警告都显示
Os：优化尺寸，该选项使能所有所有不增加尺寸的O2优化，并且进一步根据尺寸优化
4） = -DEMBED -D_uclinux_
-D: 将-Dmacro 后的macro定义为字符串1。

以下是链接：
arm-elf-ld -L/root/uClibc/lib -L/usr/local/gnu/arm-elf/lib -L/usr/local/gnu/lib/gcc-lib/arm-elf/3.0.1 -elf2flt –o hello /root/uClibc/lib/crt0.o /usr/local/gnu/lib/gcc-lib/arm-elf/3.0.1/crtbegin.o hello.o
/usr/local/gnu/lib/gcc-lib/arm-elf/3.0.1/crtend.o -lc -lgcc –lc

其中
1） 链接工具： arm-elf-ld
2） -L指明需要链接的库的路径，用法和-I一样，自己的库的路径也可以在这里加入。 -L/root/uClibc/lib -L/usr/local/gnu/arm-elf/lib 
-L/usr/local/gnu/lib/gcc-lib/arm-elf/3.0.1
3) –o 后面紧跟生成的最终的文件名
4）/root/uClibc/lib/crt0.o /usr/local/gnu/lib/gcc-lib/arm-elf/3.0.1/crtbegin.o OBJECTS.o
/usr/local/gnu/lib/gcc-lib/arm-elf/3.0.1/crtend.o
这是需要链接在一起的.o文件
5） -lc -lgcc –lc -l 后面紧跟的是需要链接的库的名字，一般库的名字是libxxx.a,使用时为-lxxx即可，不加lib和.a。还要注意位置，自己的库文件应该加在他的库前面。

编译通过后，移植就算完成了，对于比较小的源代码都可以这样，即先分析他的编译选项（用到了那些头文件，库文件等），然后用自己的编译器对照相应参数重新编译一下就行了。
当然这只是简单程序的移植，复杂案例在下一次讲吧。
 

]]></description>
<pubDate>
2006-03-17 09:53:50.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308691.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308691.shtml#comment</comments>
</item>
<item>
<blogcn_uid>
<![CDATA[525912]]></blogcn_uid>
<title>
<![CDATA[已经移植到uCLinux下的主要用户程序简介]]></title>
<link>
http://wangbangjie.blogcn.com/diary,102308787.shtml</link>
<description>
<![CDATA[boa：适合于嵌入式应用的WebServer
busybox：适合于嵌入式应用的工具软件集
dhcpcd、dhcpd、dhcp-isc： dhcp协议客户端和服务器端守护进程
diald：电话拨号守护程序
ethattach：网卡添加和管理工具软件
fileutils：适合于嵌入式应用的文件管理工具软件
flashw：Flash写入程序
flatfsd：flat文件系统守护程序
freeswan：IPSEC（Internet安全协议）的Linux实现，用于VPN和Internet上的安全通信,适合VPN、网络安全产品方面的应用
ftp：ftp（文件传输协议）客户端程序
games：游戏软件
gdbserver：目标系统端远程调试程序，与主机上运行的GDB软件配合完成对目标系统上运行的程序进行远程调试的功能。
httpd：另一个适合于嵌入式应用的WebServer，比boa更简单占用内存更少。
ipchains、ipfwadm、ipportfw、ipredir：Linux下的防火墙软件。
jffs-tools：jffs文件系统（适合Flash存储器的）工具软件。
lcd：用于测试lcddma 设备驱动程序的软件。
levee：一个面向屏幕的编辑器。
lirc：Linux 红外遥控软件包，支持串口、并口及一些商品化的红外接收器和发射器。
Login：一个简单的login程序
Mail：电子邮件收发软件
Maradns：DNS server程序，安全、简单、高效
Mbus：I2C总线设备驱动程序的测试程序
Mclock：PCF8563实时时钟应用程序
mp3play：uCLinux下的mp3播放程序
netflash：通过以太网写入Flash的程序
nwsh：一个bash的替代品，占用内存更少
ping：网络测试程序ping
play：wav文件播放程序
portmap：TCP端口映射（port map）程序
pppd、pppd.small：PPP协议uCLinux实现，支持按需拨号
pptp-client、pptpd：pptp协议的Linux实现
python：一个面向对象的编程语言，适合分布式应用，类似Perl、Java Script。
Readprofile：读取/proc/profile的内容的程序。
Reiserfsprogs：基于平衡树算法的文件系统。
Reload：将二进制可执行文件装入DRAM并执行。
Route：包括用于控制Linux内核网络子系统的一些重要工具软件，包括arp,hostname, ifconfig,netstat, rarp 和route. 
Routed：实现网络路由功能的程序
rp-pppoe：一个重定向程序，用于PPPoE（Pppp over Ethnet），ADSL设备里经常用到
samba：桑巴，支持SMB和CIFS协议，使Linux能够与Windows、Unix机器共享文件、打印机以及其他信息。
sash 、sh：经过改进后的Shell程序
shutils：适合于嵌入式应用的Shell工具
smbmount：Linux SMB client程序
smtpclient：简单的小型SMTP client程序
snmpd：SNMP协议守护程序
squid：WWW Cache软件
tcpdump：TCP流量统计软件
telnet、telnetd：telnet client和server端应用程序
tftpd：tftp协议server端应用程序
thttpd：一个小的Webserver
tinylogin: 一个小的login程序
tip：串口连接程序
traceroute：网络管理工具软件，能够跟踪进入和离开系统的IP包的路径
tripwire：系统管理工具软件，能够检测 特定文件是否被修改过
ucdsnmp：一个SNMP协议应用软件包
vplay：音频播放程序
wget：网络工具软件包，使用HTTP和FTP协议从World Wide Web上下载文件
winsd：Winserver守护进程，可以让Linux系统看到Windows网络邻居
zebra：一个功能强大的IP路由软件包，支持BGP4, BGP4+, OSPFv2, OSPFv3, RIPv1, RIPv2, RIPng等多种Internet路由协议 
]]></description>
<pubDate>
2006-03-17 09:38:01.0</pubDate>
<guid>
http://wangbangjie.blogcn.com/diary,102308787.shtml</guid>
<comments>
http://wangbangjie.blogcn.com/diary,102308787.shtml#comment</comments>
</item>
</channel>
</rss>