免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 5060 | 回复: 0
打印 上一主题 下一主题

杂文 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-06-18 11:00 |只看该作者 |倒序浏览
static/extern/const等释疑
njustcxy(原作)
下面关于C++的几个关键字是经常和我们打交道的而我们又经常对这些含糊不清的,本文根据自己的学习体会作以总结,以期达到真正理解和活用的目的。
staticl         静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可改变其值。
l         静态变量或静态函数,即只有本文件内的代码才可访问它,它的名字(变量名或函数名)在其它文件中不可见。
l         在函数体内生成的静态变量它的值也只能维持
int max_so_far( int curr )//求至今(本次调用)为止最大值
{
   static int biggest; //该变量保持着每次调用时的最新值,它的有效期等于整个程序的有效期
   if( curr > biggest )
      biggest = curr;
   return biggest;
}
l         在C++类的成员变量被声明为static(称为静态成员变量),意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;而类的静态成员函数也只能访问静态成员(变量或函数)。
l         类的静态成员变量必须在声明它的文件范围内进行初始化才能使用,private类型的也不例外。如,               float SavingsAccount::currentRate = 0.00154;   (注:currentRate是类SavingsAccount的静态成员变量)registerl         用register声明的变量称着寄存器变量,在可能的情况下会直接存放在机器的寄存器中;但对32位编译器不起作用,当global optimizations(全局优化)开的时候,它会做出选择是否放在自己的寄存器中;不过其它与register关键字有关的其它符号都对32位编译器有效。
autol         它是存储类型标识符,表明变量(自动)具有本地范围,块范围的变量声明(如for循环体内的变量声明)默认为auto存储类型。
externl         声明变量或函数为外部链接,即该变量或函数名在其它文件中可见。被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。用其声明的变量或函数应该在别的文件或同一文件的其它地方定义(实现)。在文件内声明一个变量或函数默认为可被外部使用。
l         在C++中,还可用来指定使用另一语言进行链接,这时需要与特定的转换符一起使用。目前Microsoft C/C++仅支持”C”转换标记,来支持C编译器链接。使用这种情况有两种形式:
u       extern “C” 声明语句
u       extern “C” { 声明语句块 }
volatilel         限定一个对象可被外部进程(操作系统、硬件或并发线程等)改变,声明时的语法如下:
int volatile nVint;
       这样的声明是不能达到最高效的,因为它们的值随时会改变,系统在需要时会经常读写这个对象的值。       只常用于像中断处理程序之类的异步进程进行内存单元访问。
constl         const所修饰的对象或变量不能被改变,修饰函数时,该函数不能改变在该函数外面声明的变量也不能调用任何非const函数。在函数的声明与定义时都要加上const,放在函数参数列表的最后一个括号后。
l         在C++中,用const声明一个变量,意味着该变量就是一个带类型的常量,可以代替#define,且比#define多一个类型信息,且它执行内链接,可放在头文件中声明;但在C中,其声明则必须放在源文件(即.C文件)中,在C中const声明一个变量,除了不能改变其值外,它仍是一具变量,如
const int maxarray = 255;
char store_char[maxarray];  //C++中合法,C中不合法
l         const修饰指针时要特别注意。例:
char *const aptr = mybuf;  // 常量指针*aptr = 'a';       // Legalaptr = yourbuf;    // Errorconst char *bptr = mybuf;  // (指针bptr)指向常量数据*bptr = 'a';       // Errorbptr = yourbuf;    // Legall         const修饰成员函数时不能用于构造和析构函数。
作者

teren
(   
技术
  )
  
::
最新回复
(0) ::
   静态链接网址 ::
   引用 (0)
   
         
       -->
宏的妙用
作者
阿荣

1、概述
C++中出了const关键字以后,宏定义常量的功能已经不在被推荐使用。这使得宏似乎没有了用武之地。实际上,宏还可以做很多事情,笔者也难以全部列举。这里,仅仅列举几个典型的用法,希望大家能够从中获益。
2、实现多环境兼容

见的情况是,我们实现了一个函数,希望它只在某种编译条件满足是被编译和使用。例如,我希望在源码中插入调试语句,以便以Debug方式运行时能够通过调
试信息观察程序运行情况。但是,在产品发售给用户时,我又希望这些调试信息不要输出,以降低代码尺寸,提高运行性能。
这一问题的解决方法就是使用宏。根据条件编译指令,对于不同的编译条件,提供不同的实现。例如:我们希望在特定的位置向日志中写入当前行号和文件名,以判
断对应代码是否被执行到,可以使用下面的宏:
        #ifdef _DEBUG
        #define TRACE_FILE_LINE_INFO() do{
            CString str;
            str.Format(_T("file=%s,line=%urn",__FILE__,__LINE__);
            CFile file("logfile.txt");
            file.Write(str,str.GetLength());
       }while(0)
       #else
       #define TRACE_FILE_LINE_INFO()
       #endif
上面这段代码通过#ifdef #else #endif三个条件编译指令,根据_DEBUG定义情况(该宏用于区分DEBUG版本和Release版本),决定了具体的TRACE_FILE_LINE_INFO宏函数的实现。使用者可以用如下方法使用
    TRACE_FILE_LINE_INFO();//这里显示行号和文本信息
当然,采用其他方式也可以实现这一功能,但是使用宏有以下特殊好处:
只有需要的代码才会被编译,减少了符号表的尺寸,也减少了代码尺寸
宏在编译时被展开,因此用于表示代码位置的__FILE__,__LINE__宏可以起作用,如果用函数实现,这两个宏则不能起作用。
3、用新函数替换原有函数

于一个设计好的函数,假设它已经在一个很大的工程中到处使用,突然发现它的一个不足,想修改它的功能。也许这个新增加的功能需要一个额外的参数,但是又不
想修改使用这些函数的地方。
假设有两个函数必须成对使用,一个占用资源并使用,另外一个则释放资源以供其他模块使用。典型的例子是,函数一(假设为Lock)获得一个全局的锁,这个
锁用于保护在多线程情况下多个线程对一个公共资源如一个全局变量的访问。问题是,这个Lock函数获得锁以后,其他线程将不能再获得这个锁,直到当前线程
释放这个锁。编制Lock函数的程序员同时提供了一个
Unlock函数用于释放锁,并要求使用Lock的人必须对应的使用Unlock。调试程序时,发现线程被死锁,怀疑有人使用完Lock后忘记调用
Unlock,但是Lock和Unlock在这个大工程中都被广泛的使用,因此设计者希望Lock和Unlock都增加两个额外的参数file和
line,以说明这两个函数在哪里被调用了,哪些地方被死锁以及哪些地方调用了Lock但是没有调用Unlock。 假设这两个函数的原型为:
        void Lock();
        void Unlock();
新设计的函数的原型是:
        void Lock(LPCTSTR szFileName,UINT uLineNo);
        void Unlock(LPCTSTR szFileName,UINT uLineNo);
设计完新的函数后,项目经理希望所有模块统一使用这两个函数并提供文件名和行号信息作为
参数。这样将是一个非常浩大且烦琐的工作,意味着重复性的劳动、数小时无聊的加班和工期的延误,这是谁都不愿意遇到的。
使用宏可以非常轻松的解决这一切。首先,应该把新设计的函数换个名字,不妨叫它们NewLock和NewUnlock,也就是他们的原型为:
        void NewLock(LPCTSTR szFileName,UINT uLineNo);
        void NewUnlock(LPCTSTR szFileName,UINT uLineNo);
这个函数原型应该放在一个头文件中,避免在多个地方重复的声明。需要用到这两个函数的cpp文件,只要包含他们原型所在的头文件即可。为了不改动使用Lock/Unlock函数的模块,在头文件中增加如下两行:
    #define Lock() NewLock(__FILE__,__LINE__)
    #define Unlock() NewUnlock(__FILE,__LINE__)
这样,当不同模块使用这个函数时,宏替换功能在编译时起作用,自动使用了__FILE__和__LINE__为参数,调用了新设计的函数。调试的时候就可以根据日志来判断什么地方遗漏了调用Unlock。
4、给一个函数捆绑其他功能

述方法修改了原来函数的设计。实际上,这两个函数本身没有问题,只是使用者使用上出了问题。你可能只需要在调试版本中测试到底谁遗漏了这些重要信息。对于
一些严谨的公司,一旦软件被修改,推出销售前就需要进行严格的测试。因此项目经理可能不会允许修改原有函数的设计,要求直接捆绑一个测试代码。产品发售
时,删除捆绑代码即可。
使用宏也可以捆绑代码,这需要首先了解一个宏的特点:如果你的代码中出现了一个字符串,编译器会首先匹配宏,并试图用宏展开。这样,即使你有同名的函数,
它也不会被当作函数处理。但是,如果一个宏展开时发现,展开式是一个嵌套的宏展开,展开式就试图在进入下一次嵌套展开之前,试图用函数匹配来终止这种无限
循环。 为此,定义如下两个宏:
    #define Lock() Lock();
        TRACE("Lock called in file = %s at line =%un",__FILE__,__LINE__)
    #define Unlock() Unlock();   
        TRACE("Unlock called in file = %s at line =%un",__FILE__,__LINE__)
编译器在编译过程中,发现如下代码
        //here the Lock function is called
        Lock();
它首先把这个Lock理解成宏函数,展开成:
        //here the Lock function is called
        Lock();
        TRACE("Lock called in file = %s at line = %un",__FILE__,__LINE__);
上述代码中,__FILE__和__LINE__应该同时被展开,由于与论题无关,所以
还是原样给出。展开以后,Lock还是一个和宏匹配的式子,但是编译器发现如果这样下去,它将是一个无休止的迭代,因此它停止展开过程,讯中同名的函数,
因此上面的代码已经是最终展开式。
这样,我们成功的不改变Lock函数的原型和设计,捆绑了一条调试信息上去。由于TRACE语句在Release版本中不会出现,这样也避免了不得不进行
额外的测试过程。
5、实现一些自动化过程
程序中需要输入一组参数,为此设计了一个对话框来输入。问题是:每次显示对话框时,都希望能按照上次输入的值显示。设计当然没有问题,在文档中保存输入的参数,在显示对话框前在把保存的值赋值给对话框对应控制变量。下面是常见的代码:
    CMyDoc * pDoc = GetDocument();
    ASSERT_VALID(pDoc);
   
    CParameterDlg dlg;
    //设置对话框初值
    dlg.m_nValue1   = pDoc->m_nValue1;
    dlg.m_szValue2  = pDoc->m_szValue2;
    ......
    dlg.m_lValuen   = pDoc->m_lValuen;
    //显示对话框
    if(dlg.DoModal() == IDOK)
    {
       //点击OK按钮后保存设置
        pDoc->m_nValue1  = dlg.m_nValue1;
        pDoc->m_szValue2 = dlg.m_szValue2;
            ......
        pDoc->m_lValuen  = dlg.m_lValuen;
    }
如果整个程序只有一两个这样的代码段,并且每个代码段涉及的变量个数都很少,当然没有问
题,但是当你程序中有成百上千个这样的参数对话框,每个对话框又对应数十个这样的参数,工作量就非常可观了(而且是没有任何成就感的工作量)。我想,用
VC做界面的朋友们大多遇到过这样的问题。可以注意到,上述代码在DoModal前后都是一组赋值过程,但是赋值的方向不是很一致,因此每个变量对都需要
写两个赋值语句。那么是否可以做一个函数,前后各调用一次,根据一个参数决定方向。而且函数中也只需要对每个变量写一次?
下面这个函数就是一个实现:
    void DataExchange(CMyDoc * pMyDoc,CParameterDlg * pDlg,BOOL flag )
    {
        BEGIN_EXCHANGE(pMyDoc,CMyDoc,pDlg,CParameterDlg,flag)
        EXCHANGE(m_nValue1);
        EXCHANGE(m_szValue2);
                ....
        EXCHANGE(m_lValue2);
        END_EXCHANGE()
    }
为了使上述语义能起作用,定义上面三个宏如下:
    #define BEGIN_EXCHANGE(left,lefttype,right,righttype,flag)
        {
            CSmartPtr pLeft   = left;
            CSmartPtr pRight = right
            
    #define END_EXCHANGE() }
    #define EXCHANGE(varible)
        if(flag)
        {
            pLeft->varible = pRight->varible ;
        }else{
            pRight->varible = pLeft->varible;
        |
这里为了避免每次都输入varible所属对象的指针,使用了一个智能指针来提供一个左指针pLeft和一个右指针pRight语义,这个智能指针只需要实现取下标功能即可,因此可以简单实现如下(为了通用,必须为模板类):
    template
    class CSmartPointer
    {
        protected:
            TYPE * m_pPointer;
        public:
            CSmartPointer(TYPE * pPointer):m_pPointer(pPointer){};
            TYPE* operator->() {return m_pPointer;}
    };
这样,原来的代码就可以修改成这样:
    CMyDoc * pDoc = GetDocument();
    ASSERT_VALID(pDoc);
   
    CParameterDlg dlg;
    //设置对话框初值
    DataExchange(pDoc,&dlg,FALSE);
    //显示对话框
    if(dlg.DoModal() == IDOK)
    {
       //点击OK按钮后保存设置
        DataExchange(pDoc,&dlg,TRUE);
    }  
上述代码要求左右指针对应变量名必须相同,如果变量名不同,就不能这样使用,需要设计成这样的EXCHANGE2宏:
    #define EXCHANGE2(leftvar,rightvar)
        if(flag)
        {
            pLeft->leftvar,pRight->rightvar;
        }else{
            pRight->rightvar = pLeft->leftvar;
        }
这样,对应的EXCHANGE子句需要修改成
       EXCHANGE2(m_lValue1,m_dwValue2);
上述代码看起来是完美的,但是有一些特殊还是不正确,这些特殊情况就是=用于赋值不正确的情况。
有两种常见问题:
  • leftvar和rightvar分别是指针类型,但是其实想拷贝它们指向的缓冲区的内容(如字符串拷贝)。
  • 为了控制显示精度,对话框控制变量是一个CString对象,它是文档对象中对应变量的格式化后的信息。最常见的是, leftvar是一个浮点数,需要以几个小数位格式输出,因此rightvar是一个CString对象。
    为了实现上面的目的,就不能使用=来直接赋值,而应该用一个函数Assign(函数名当然可以任意取啦)来做这件事。为此,修改上述的EXCHANGE和EXCHANGE2宏如下:
        #define EXCHANGE(var)
            if(flag)
            {
                Assign(pLeft->var,pRight->var);
            }else{
                Assign(pRight->var,pLeft->var);
            }
       #define EXCHANGE2(leftvar,rightvar)
           if(flag)
            {
                Assign(pLeft->leftvar,pRight->rightvar);
            }else{
                Assign(pRight->rightvar,pLeft->leftvar);
            }      
    这样只要针对每个类型对实现一次Assign即可。由于C++允许重载,这显得很容易。需要实现的函数一般有:
      函数功能void Assign(CString & left,CString & right)直接赋值CString类型void Assign(CString & left, float & fValue)格式化float数值到leftvoid Assign(float & fValue,CString & right)从字符串中读取出floatvoid Assign(CString & left, double& dValue)格式化double数值到leftvoid Assign(double& dValue,CString & right)从字符串中读取出doublevoid Assign(CString & left, int & iValue)格式化int数值到leftvoid Assign(int & iValue,CString & right)从字符串中读取出intvoid Assign(CString & left, short& sValue)格式化short数值到leftvoid Assign(short & sValue,CString & right)从字符串中读取出shortvoid Assign(CString & left, long & lValue)格式化long数值到leftvoid Assign(long & lValue,CString & right)从字符串中读取出longvoid Assign(CString & left, CTime & time)格式化CTime数值到leftvoid Assign(CTime & time,CString & right)从字符串中读取出CTime
    到底要实现哪些类型对,需要读者根据自己项目需要设计。
    小结
    宏的功能应该还有许多,但是我才疏学浅,只能想到这么一点,希望能对大家有所帮助。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期三, 六月 14, 2006
          
           -->
    Identifying Memory Leaks in Linux for C++ Programs
    Most C++ programmers agree that it can be harrowing trying to identify the memory leaks in a given program.
    If you're working on the GNU/Linux platform, there's an interesting tool you can use to minimize the hassle of this task: mtrace.
    Here's some background on mtrace:
  • You call the mtrace()
    function to log all memory leaks. The memory allocations and
    deallocations are logged to a text file pointed to by the environment
    variable—MALLOC_TRACE.
  • A Perl utility called mtrace parses the text file logged by your program and identifies the memory leaks. The following code allocates memory, but does not essentially free it:
    #include
    int main() {
            int *a;
            a = malloc(sizeof(int)); //Allocate memory
           
            *a = 7;
            //Notice that we are not freeing memory before we end the program.
           
            return EXIT_SUCCESS;
    }
    Now, see how to use mtrace to identify the memory leak:
    • Step 1: Setup MALLOC_TRACE environment variable to point to a file where mtrace needs to log the memory allocations:   
         setenv MALLOC_TRACE  /home/karthik/temp/trace.txt
    • Step 2: Insert mtrace hooks into the program:
      #include
      #include  /* Header file to include mtrace related functions */
      int main() {
              int *a;
              mtrace(); /* This starts memory tracing.
      This has to be done before we do a 'malloc' or we allocate memory.  */
             
              a = malloc(sizeof(int));  /* Allocate memory */
             
              *a = 7;
              /* Notice that we are not freeing memory before we end the program.  */
             
              return EXIT_SUCCESS;
      }
    • Step 3: Compile the modified program with the debugging options turned on:
          $ gcc -g -Wall -ansi -pedantic leak.c
    • Step 4: Run the program.
    • Step 5: Use the mtrace utility to retrieve the information. Here's what the syntax looks like:
      mtrace   
      [akkumar@empress work]$ mtrace a.out ~/temp/trace.txt
      Memory not freed:
      -----------------
         Address     Size     Caller
      0x08049910      0x4  at /home/karthik/tips/leak.c:9
    This precisely tells you that there is a potential memory leak at line 9:
      a = malloc(sizeof(int));  /* Allocate memory */
    mtrace is a GNU utility.
    The code in this tip was tested on a Linux platform with the gcc 3.2.3.
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    在linux下面使用mtrace来检查一般程序的内存溢出
    kan.bbs@bbs.whu.edu.cn
    在linux下面使用mtrace来检查一般的程序的内存溢出
    对于内存溢出之类的麻烦可能大家在编写指针比较多的复杂的程序的时候就会遇到。De
    bug起来也是比较累人。其实linux系统下有一个使用的工具可以帮忙来调试的,这就是
    mtrace。Mtrace主要能够检测一些内存分配和泄漏的失败等。下面我们来学习一下它的
    用法。
    使用mtrace来调试程序有4个基本的步骤,需要用到GNU C 函数库里面的一些辅助的函数
    功能。
    1. 在需要跟踪的程序中需要包含头文件,而且在main()函数的最开始包含
    一个函数调用:mtrace()。由于在main函数的最开头调用了mtrace(),所以该进程后面
    的一切分配和释放内存的操作都可以由mtrace来跟踪和分析。
    2. 定义一个环境变量,用来指示一个文件。该文件用来输出log信息。如下的例子:
    $export MALLOC_TRACE=mymemory.log
    3. 正常运行程序。此时程序中的关于内存分配和释放的操作都可以记录下来。
    4. 然后用mtrace使用工具来分析log文件。例如:
    $mtrace testmem $MALLOC_TRACE
    下面我们看一个例子:
    [hwang@langchao test]$ cat testmtrace.c
    #include
    #include
    #include
    int main()
    {
            char *hello;
            mtrace();
            hello = (char*) malloc(20);
            sprintf(hello,"nhello world!");
            return 1;
    }
    [hwang@langchao test]$export MALLOC_TRACE=mytrace.log
    [hwang@langchao test]$ gcc testmtrace.c -o testmtrace
    [hwang@langchao test]$./testmtrace
    [hwang@langchao test]$ mtrace testmtrace mytrace.log
    Memory not freed:
    -----------------
       Address Size Caller
    0x08049860 0x14 at /usr/src/build/53700-i386/BUILD/glibc-2.2.4/csu/init.c:0
    ---
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期五, 六月 09, 2006
          
           -->
    循环冗余校验CRC的算法分析和程序实现
    概述
    在数字通信系统中可靠与快速往往是一对矛盾。若要求快速,则必然使得每个数据码元所占
    地时间缩短、波形变窄、能量减少,从而在受到干扰后产生错误地可能性增加,传送信息地可靠性下降。若是要求可靠,则使得传送消息地速率变慢。因此,如何合
    理地解决可靠性也速度这一对矛盾,是正确设计一个通信系统地关键问题之一。为保证传输过程的正确性,需要对通信过程进行差错控制。差错控制最常用的方法是
    自动请求重发方式(ARQ)、向前纠错方式(FEC)和混合纠错(HEC)。在传输过程误码率比较低时,用FEC方式比较理想。在传输过程误码率较高时,
    采用FEC容易出现“乱纠”现象。HEC方式则式ARQ和FEC的结合。在许多数字通信中,广泛采用ARQ方式,此时的差错控制只需要检错功能。实现检错
    功能的差错控制方法很多,传统的有:奇偶校验、校验和检测、重复码校验、恒比码校验、行列冗余码校验等,这些方法都是增加数据的冗余量,将校验码和数据一
    起发送到接受端。接受端对接受到的数据进行相同校验,再将得到的校验码和接受到的校验码比较,如果二者一致则认为传输正确。但这些方法都有各自的缺点,误
    判的概率比较高。
    循环冗余校验CRC(Cyclic Redundancy Check)是由分组线性码的分支而来,其主要应用是二元码组。编码简单且误判概率很低,在通信系统中得到了广泛的应用。下面重点介绍了CRC校验的原理及其算法实现。
    一、循环冗余校验码(CRC)
    CRC校验采用多项式编码方法。被处理的数据块可以看作是一个n阶的二进制多项式,由 。如一个8位二进制数10110101可以表示为: 。多项式乘除法运算过程与普通代数多项式的乘除法相同。多项式的加减法运算以2为模,加减时不进,错位,和逻辑异或运算一致。
    采用CRC校验时,发送方和接收方用同一个生成多项式g(x),并且g(x)的首位和最后一位的系数必须为1。CRC的处理方法是:发送方以g(x)去除t(x),得到余数作为CRC校验码。校验时,以计算的校正结果是否为0为据,判断数据帧是否出错。
    CRC
    校验可以100%地检测出所有奇数个随机错误和长度小于等于k(k为g(x)的阶数)的突发错误。所以CRC的生成多项式的阶数越高,那么误判的概率就越
    小。CCITT建议:2048 kbit/s的PCM基群设备采用CRC-4方案,使用的CRC校验码生成多项式g(x)=
    。采用16位CRC校验,可以保证在 bit码元中只含有一位未被检测出的错误
    。在IBM的同步数据链路控制规程SDLC的帧校验序列FCS中,使用CRC-16,其生成多项式g(x)=
    ;而在CCITT推荐的高级数据链路控制规程HDLC的帧校验序列FCS中,使用CCITT-16,其生成多项式g(x)=
    。CRC-32的生成多项式g(x)= 。CRC-32出错的概率比CRC-16低 倍
    。由于CRC-32的可靠性,把CRC-32用于重要数据传输十分合适,所以在通信、计算机等领域运用十分广泛。在一些UART通信控制芯片(如
    MC6582、Intel8273和Z80-SIO)内,都采用了CRC校验码进行差错控制;以太网卡芯片、MPEG解码芯片中,也采用CRC-32进行
    差错控制。
    二、CRC校验码的算法分析
    CRC校验码的编码方法是用待发送的二进制数据t(x)除以生成多项式g(x),将最后的余数作为CRC校验码。其实现步骤如下:
    (1) 设待发送的数据块是m位的二进制多项式t(x),生成多项式为r阶的g(x)。在数据块的末尾添加r个0,数据块的长度增加到m+r位,对应的二进制多项式为 。
    (2) 用生成多项式g(x)去除 ,求得余数为阶数为r-1的二进制多项式y(x)。此二进制多项式y(x)就是t(x)经过生成多项式g(x)编码的CRC校验码。
    (3) 用 以模2的方式减去y(x),得到二进制多项式 。 就是包含了CRC校验码的待发送字符串。
    从CRC
    的编码规则可以看出,CRC编码实际上是将代发送的m位二进制多项式t(x)转换成了可以被g(x)除尽的m+r位二进制多项式
    ,所以解码时可以用接受到的数据去除g(x),如果余数位零,则表示传输过程没有错误;如果余数不为零,则在传输过程中肯定存在错误。许多CRC的硬件解
    码电路就是按这种方式进行检错的。同时
    可以看做是由t(x)和CRC校验码的组合,所以解码时将接收到的二进制数据去掉尾部的r位数据,得到的就是原始数据。
    为了更清楚的了解
    CRC校验码的编码过程,下面用一个简单的例子来说明CRC校验码的编码过程。由于CRC-32、CRC-16、CCITT和CRC-4的编码过程基本一
    致,只有位数和生成多项式不一样。为了叙述简单,用一个CRC-4编码的例子来说明CRC的编码过程。
    设待发送的数据t(x)为12位的二进
    制数据100100011100;CRC-4的生成多项式为g(x)= ,阶数r为4,即10011。首先在t(x)的末尾添加4个0构成
    ,数据块就成了1001000111000000。然后用g(x)去除 ,不用管商是多少,只需要求得余数y(x)。下表为给出了除法过程。
    除数次数 被除数/ g(x)/结果 余数
    0 1 001000111000000 100111000000
    1 0011
    0 000100111000000
    1 1 00111000000 1000000
    1 0011
    0 00001000000
    2 1 000000 1100
    1 0011
    0 001100
    从上面表中可以看出,CRC编码实际上是一个循环移位的模2运算。对CRC-4,我们假设有一个5 bits的寄存器,通过反复的移位和进行CRC的除法,那么最终该寄存器中的值去掉最高一位就是我们所要求的余数。所以可以将上述步骤用下面的流程描述:
    //reg是一个5 bits的寄存器
    把reg中的值置0.
    把原始的数据后添加r个0.
    While (数据未处理完)
    Begin
    If (reg首位是1)
    reg = reg XOR 0011.
    把reg中的值左移一位,读入一个新的数据并置于register的0 bit的位置。
    End
    reg的后四位就是我们所要求的余数。

    种算法简单,容易实现,对任意长度生成多项式的G(x)都适用。在发送的数据不长的情况下可以使用。但是如果发送的数据块很长的话,这种方法就不太适合
    了。它一次只能处理一位数据,效率太低。为了提高处理效率,可以一次处理4位、8位、16位、32位。由于处理器的结构基本上都支持8位数据的处理,所以
    一次处理8位比较合适。
    为了对优化后的算法有一种直观的了解,先将上面的算法换个角度理解一下。在上面例子中,可以将编码过程看作如下过程:

    于最后只需要余数,所以我们只看后四位。构造一个四位的寄存器reg,初值为0,数据依次移入reg0(reg的0位),同时reg3的数据移出reg。
    有上面的算法可以知道,只有当移出的数据为1时,reg才和g(x)进行XOR运算;移出的数据为0时,reg不与g(x)进行XOR运算,相当与和
    0000进行XOR运算。就是说,reg和什么样的数据进行XOR移出的数据决定。由于只有一个bit,所以有 种选择。上述算法可以描述如下,
    //reg是一个4 bits的寄存器
    初始化t[]={0011,0000}
    把reg中的值置0.
    把原始的数据后添加r个0.
    While (数据未处理完)
    Begin
    把reg中的值左移一位,读入一个新的数据并置于register的0 bit的位置。
    reg = reg XOR t[移出的位]
    End

    面算法是以bit为单位进行处理的,可以将上述算法扩展到8位,即以Byte为单位进行处理,即CRC-32。构造一个四个Byte的寄存器reg,初值
    为0x00000000,数据依次移入reg0(reg的0字节,以下类似),同时reg3的数据移出reg。用上面的算法类推可知,移出的数据字节决定
    reg和什么样的数据进行XOR。由于有8个bit,所以有 种选择。上述算法可以描述如下:
    //reg是一个4 Byte的寄存器
    初始化t[]={…}//共有 =256项
    把reg中的值置0.
    把原始的数据后添加r/8个0字节.
    While (数据未处理完)
    Begin
    把reg中的值左移一个字节,读入一个新的字节并置于reg的第0个byte的位置。
    reg = reg XOR t[移出的字节]
    End

    法的依据和多项式除法性质有关。如果一个m位的多项式t(x)除以一个r阶的生成多项式g(x), ,将每一位
    (0=
    unsigned long GenerateCRC32(char xdata * DataBuf,unsigned long len)
    {
    unsigned long oldcrc32;
    unsigned long crc32;
    unsigned long oldcrc;
    unsigned int charcnt;
    char c,t;
    oldcrc32 = 0x00000000; //初值为0
    charcnt=0;
    while (len--) {
    t= (oldcrc32 >> 24) & 0xFF; //要移出的字节的值
    oldcrc=crc_32_tab[t]; //根据移出的字节的值查表
    c=DataBuf[charcnt]; //新移进来的字节值
    oldcrc32= (oldcrc32  
    unsigned long int crc32_table[256];
    unsigned long int ulPolynomial = 0x04c11db7;
    unsigned long int Reflect(unsigned long int ref, char ch)
    { unsigned long int value(0);
    // 交换bit0和bit7,bit1和bit6,类推
    for(int i = 1; i >= 1; }
    return value;
    }
    init_crc32_table()
    { unsigned long int crc,temp;
    // 256个值
    for(int i = 0; i
    结束语
    CRC校验由于实现简单,检错能力强,被广泛使用在各种数据校验应用中。占用系统资源少,用软硬件均能实现,是进行数据传输差错检测地一种很好的手段。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期二, 六月 06, 2006
          
           -->
    linux下的内存管理
    Tag
    malloc
                                              
    动态分配的选择、折衷和实现
    级别: 中级
    Jonathan Bartlett

    johnnyb@eskimo.com

    技术总监, New Media Worx
    2004 年 11 月

    文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C
    语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,
    以及如何使用垃圾收集自动管理内存。
    为什么必须管理内存

    存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理
    解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和
    C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。

    溯到在 Apple II
    上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内
    存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。
    不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求:
    • 确定您是否有足够的内存来处理数据。
    • 从可用的内存中获取一部分内存。
    • 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。


    实现这些需求的程序库称为分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。
    C 风格的内存分配程序
    C 编程语言提供了两个函数来满足我们的三个需求:
    • malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
    • free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。


    物理内存和虚拟内存
    要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是虚拟内存。

    是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM
    中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM
    已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转
    换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM
    中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用
    的地址空间,可以访问比您物理上安装的内存更多的内存。
    在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上,每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为系统中断点(system break)的
    特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM
    或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统"映射进来(map
    in)"更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。)
    基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:
    • brk:brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
    • mmap:mmap(),或者说是"内存映像",类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。munmap() 所做的事情与 mmap() 相反。


    如您所见,brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。
    实现一个简单的分配程序
    如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。
    要试着运行这些示例,需要先
    复制本代码清单
    ,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。
    在大部分操作系统中,内存分配由以下两个简单的函数来处理:
    • void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
    • void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的"空闲空间"。

    malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:
    清单 1. 我们的简单分配程序的全局变量
    int has_initialized = 0;
    void *managed_memory_start;
    void *last_valid_address;
    如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:
    清单 2. 分配程序初始化函数
    /* Include the sbrk function */
    #include
    void malloc_init()
    {
            /* grab the last valid address from the OS */
            last_valid_address = sbrk(0);
            /* we don't have any memory to manage yet, so
             *just set the beginning to be last_valid_address
             */
            managed_memory_start = last_valid_address;
            /* Okay, we're initialized and ready to go */
            has_initialized = 1;
    }
    现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此,malloc 返回的每块内存的起始处首先要有这个结构:
    清单 3. 内存控制块结构定义
    struct mem_control_block {
            int is_available;
            int size;
    };
    现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。
    在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:
    清单 4. 解除分配函数
    void free(void *firstbyte) {
            struct mem_control_block *mcb;
            /* Backup from the given pointer to find the
             * mem_control_block
             */
            mcb = firstbyte - sizeof(struct mem_control_block);
            /* Mark the block as being available */
            mcb->is_available = 1;
            /* That's It!  We're done. */
            return;
    }
    如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:
    清单 5. 主分配程序的伪代码
    1. If our allocator has not been initialized, initialize it.
    2. Add sizeof(struct mem_control_block) to the size requested.
    3. start at managed_memory_start.
    4. Are we at last_valid address?
    5. If we are:
       A. We didn't find any existing space that was large enough
          -- ask the operating system for more and return that.
    6. Otherwise:
       A. Is the current space available (check is_available from
          the mem_control_block)?
       B. If it is:
          i)   Is it large enough (check "size" from the
               mem_control_block)?
          ii)  If so:
               a. Mark it as unavailable
               b. Move past mem_control_block and return the
                  pointer
          iii) Otherwise:
               a. Move forward "size" bytes
               b. Go back go step 4
       C. Otherwise:
          i)   Move forward "size" bytes
          ii)  Go back to step 4
    我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:
    清单 6. 主分配程序
    void *malloc(long numbytes) {
            /* Holds where we are looking in memory */
            void *current_location;
            /* This is the same as current_location, but cast to a
             * memory_control_block
             */
            struct mem_control_block *current_location_mcb;
            /* This is the memory location we will return.  It will
             * be set to 0 until we find something suitable
             */
            void *memory_location;
            /* Initialize if we haven't already done so */
            if(! has_initialized)         {
                    malloc_init();
            }
            /* The memory we search for has to include the memory
             * control block, but the users of malloc don't need
             * to know this, so we'll just add it in for them.
             */
            numbytes = numbytes + sizeof(struct mem_control_block);
            /* Set memory_location to 0 until we find a suitable
             * location
             */
            memory_location = 0;
            /* Begin searching at the start of managed memory */
            current_location = managed_memory_start;
            /* Keep going until we have searched all allocated space */
            while(current_location != last_valid_address)
            {
                    /* current_location and current_location_mcb point
                     * to the same address.  However, current_location_mcb
                     * is of the correct type, so we can use it as a struct.
                     * current_location is a void pointer so we can use it
                     * to calculate addresses.
                     */
                    current_location_mcb =
                            (struct mem_control_block *)current_location;
                    if(current_location_mcb->is_available)
                    {
                            if(current_location_mcb->size >= numbytes)
                            {
                                    /* Woohoo!  We've found an open,
                                     * appropriately-size location.
                                     */
                                    /* It is no longer available */
                                    current_location_mcb->is_available = 0;
                                    /* We own it */
                                    memory_location = current_location;
                                    /* Leave the loop */
                                    break;
                            }
                    }
                    /* If we made it here, it's because the Current memory
                     * block not suitable; move to the next one
                     */
                    current_location = current_location +
                            current_location_mcb->size;
            }
            /* If we still don't have a valid location, we'll
             * have to ask the operating system for more memory
             */
            if(! memory_location)
            {
                    /* Move the program break numbytes further */
                    sbrk(numbytes);
                    /* The new memory will be where the last valid
                     * address left off
                     */
                    memory_location = last_valid_address;
                    /* We'll move the last valid address forward
                     * numbytes
                     */
                    last_valid_address = last_valid_address + numbytes;
                    /* We need to initialize the mem_control_block */
                    current_location_mcb = memory_location;
                    current_location_mcb->is_available = 0;
                    current_location_mcb->size = numbytes;
            }
            /* Now, no matter what (well, except for error conditions),
             * memory_location has the address of the memory, including
             * the mem_control_block
             */
            /* Move the pointer past the mem_control_block */
            memory_location = memory_location + sizeof(struct mem_control_block);
            /* Return the pointer */
            return memory_location;
    }
    这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。
    运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过,malloc() 和 free() 才是最主要的函数):
    清单 7. 编译分配程序
    gcc -shared -fpic malloc.c -o malloc.so
    该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。
    在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下:
    清单 8. 替换您的标准的 malloc
    LD_PRELOAD=/path/to/malloc.so
    export LD_PRELOAD
    LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。
    如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。
    我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括:
    • 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。
    • 当分配内存时,在最坏的情形下,它将不得不遍历全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。
    • 没有很好的内存不足处理方案(malloc 只假定内存分配是成功的)。
    • 它没有实现很多其他的内存函数,比如 realloc()。
    • 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。
    • 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。
    • 分配程序不是线程安全的。
    • 分配程序不能将空闲空间拼合为更大的内存块。
    • 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。
    • 我确信还有很多其他问题。这就是为什么它只是一个例子!


    其他 malloc 实现
    malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:
    • 分配的速度。
    • 回收的速度。
    • 有线程的环境的行为。
    • 内存将要被用光时的行为。
    • 局部缓存。
    • 簿记(Bookkeeping)内存开销。
    • 虚拟内存环境中的行为。
    • 小的或者大的对象。
    • 实时保证。


    每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。
    还有其他许多分配程序可以使用。其中包括:
    • Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的
      参考资料
      部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
    • BSD Malloc:BSD
      Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD
      之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2
      的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size
      类。这样就提供了一个快速的实现,但是可能会浪费内存。在
      参考资料
      部分中,有一篇描述该实现的文章。
    • Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在
      参考资料
      部分中,有一篇描述该实现的文章。



    多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程
    序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald
    Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节"Dynamic Storage Allocation"(请参阅
    参考资料
    中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。
    在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章("Small Object Allocation")中,描述了一个小对象分配程序(请参阅
    参考资料
    中的链接)。
    基于 malloc() 的内存管理的缺点
    不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc()
    来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期
    超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。
    因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。
    半自动内存管理策略
    引用计数
    引用计数是一种半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。

    引用计数中,所有共享的数据结构都有一个域来包含当前活动"引用"结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加
    1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少
    1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。
    这样
    做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结
    构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计
    数机制。引用计数也难以处理发生循环引用的数据结构。
    要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。
    一个示例引用计数函数集可能看起来如下所示:
    清单 9. 基本的引用计数函数
    /* Structure Definitions*/
    /* Base structure that holds a refcount */
    struct refcountedstruct
    {
            int refcount;
    }
    /* All refcounted structures must mirror struct
    * refcountedstruct for their first variables
    */
    /* Refcount maintenance functions */
    /* Increase reference count */
    void REF(void *data)
    {
            struct refcountedstruct *rstruct;
            rstruct = (struct refcountedstruct *) data;
            rstruct->refcount++;
    }
    /* Decrease reference count */
    void UNREF(void *data)
    {
            struct refcountedstruct *rstruct;
            rstruct = (struct refcountedstruct *) data;
            rstruct->refcount--;
            /* Free the structure if there are no more users */
            if(rstruct->refcount == 0)
            {
                    free(rstruct);
            }
    }
    REF 和 UNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是必需的)。
    当使用 REF 和 UNREF 时,您需要遵守这些指针的分配规则:
    • UNREF 分配前左端指针(left-hand-side pointer)指向的值。
    • REF 分配后左端指针(left-hand-side pointer)指向的值。


    在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则:
    • 在函数的起始处 REF 每一个指针。
    • 在函数的结束处 UNREF 第一个指针。


    以下是一个使用引用计数的生动的代码示例:
    清单 10. 使用引用计数的示例
    /* EXAMPLES OF USAGE */
    /* Data type to be refcounted */
    struct mydata
    {
            int refcount; /* same as refcountedstruct */
            int datafield1; /* Fields specific to this struct */
            int datafield2;
            /* other declarations would go here as appropriate */
    };
    /* Use the functions in code */
    void dosomething(struct mydata *data)
    {
            REF(data);
            /* Process data */
            /* when we are through */
            UNREF(data);
    }
    struct mydata *globalvar1;
    /* Note that in this one, we don't decrease the
    * refcount since we are maintaining the reference
    * past the end of the function call through the
    * global variable
    */
    void storesomething(struct mydata *data)
    {
            REF(data); /* passed as a parameter */
            globalvar1 = data;
            REF(data); /* ref because of Assignment */
            UNREF(data); /* Function finished */
    }
    由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和释放它们的内存。

    Perl
    等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容
    都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处:
    • 实现简单。
    • 易于使用。
    • 由于引用是数据结构的一部分,所以它有一个好的缓存位置。


    不过,它也有其不足之处:
    • 要求您永远不要忘记调用引用计数函数。
    • 无法释放作为循环数据结构的一部分的结构。
    • 减缓几乎每一个指针的分配。
    • 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/longjmp())时,您必须采取其他方法。
    • 需要额外的内存来处理引用。
    • 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。
    • 在多线程环境中更慢也更难以使用。


    C++ 可以通过使用智能指针(smart pointers)来
    容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C
    库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++
    项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的"Smart Pointers"那一章。
    内存池

    存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。
    例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled
    memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。

    池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache
    中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因
    此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一
    些实现允许注册清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。

    在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable
    Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable
    Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache
    的池式内存实现,请参阅
    参考资料
    部分中指向这些实现的文档的链接。
    下面的假想代码列表展示了如何使用 obstack:
    清单 11. obstack 的示例代码
    #include
    #include
    /* Example code listing for using obstacks */
    /* Used for obstack macros (xmalloc is
       a malloc function that exits if memory
       is exhausted */
    #define obstack_chunk_alloc xmalloc
    #define obstack_chunk_free free
    /* Pools */
    /* Only permanent allocations should go in this pool */
    struct obstack *global_pool;
    /* This pool is for per-connection data */
    struct obstack *connection_pool;
    /* This pool is for per-request data */
    struct obstack *request_pool;
    void allocation_failed()
    {
            exit(1);
    }
    int main()
    {
            /* Initialize Pools */
            global_pool = (struct obstack *)
                    xmalloc (sizeof (struct obstack));
            obstack_init(global_pool);
            connection_pool = (struct obstack *)
                    xmalloc (sizeof (struct obstack));
            obstack_init(connection_pool);
            request_pool = (struct obstack *)
                    xmalloc (sizeof (struct obstack));
            obstack_init(request_pool);
            /* Set the error handling function */
            obstack_alloc_failed_handler = &allocation_failed;
            /* Server main loop */
            while(1)
            {
                    wait_for_connection();
                    /* We are in a connection */
                    while(more_requests_available())
                    {
                            /* Handle request */
                            handle_request();
                            /* Free all of the memory allocated
                             * in the request pool
                             */
                            obstack_free(request_pool, NULL);
                    }
                    /* We're finished with the connection, time
                     * to free that pool
                     */
                    obstack_free(connection_pool, NULL);
            }
    }
    int handle_request()
    {
            /* Be sure that all object allocations are allocated
             * from the request pool
             */
            int bytes_i_need = 400;
            void *data1 = obstack_alloc(request_pool, bytes_i_need);
            /* Do stuff to process the request */
            /* return */
            return 0;
    }
    基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free() 的 NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。
    使用池式内存分配的益处如下所示:
    • 应用程序可以简单地管理内存。
    • 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。
    • 可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。
    • 有非常易于使用的标准实现。


    池式内存的缺点是:
    • 内存池只适用于操作可以分阶段的程序。
    • 内存池通常不能与第三方库很好地合作。
    • 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。
    • 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。

    垃圾收集
    垃圾收集(Garbage collection)是
    全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组"基本"数据
    —— 栈数据、全局变量、寄存器 ——
    作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数
    据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。
    收集器的类型
    • 复制(copying):
      这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从"基本"的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分
      现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃
      圾收集方法,垃圾收集器必须与编程语言集成在一起。
    • 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从"基本"的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。
    • 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。
    • 保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们可以全
      部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引
      发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以
      与任何编程语言相集成。


    Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD
    技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla
    严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。
    垃圾收集的一些优点:
    • 您永远不必担心内存的双重释放或者对象的生命周期。
    • 使用某些收集器,您可以使用与常规分配相同的 API。


    其缺点包括:
    • 使用大部分收集器时,您都无法干涉何时释放内存。
    • 在多数情况下,垃圾收集比其他形式的内存管理更慢。
    • 垃圾收集错误引发的缺陷难于调试。
    • 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。


    结束语

    切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大
    量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文
    中涉及的内存管理策略。
    表 1. 内存分配策略的对比
    策略分配速度回收速度局部缓存易用性通用性实时可用SMP 线程友好定制分配程序 取决于实现 取决于实现 取决于实现 很难 无 取决于实现 取决于实现 简单分配程序内存使用少时较快很快差 容易 高 否 否 GNU malloc中 快 中 容易 高 否 中Hoard 中 中 中 容易 高 否 是 引用计数 N/A N/A 非常好 中 中 是(取决于 malloc 实现) 取决于实现 池 中 非常快 极好 中 中 是(取决于 malloc 实现) 取决于实现 垃圾收集 中(进行收集时慢) 中 差 中 中 否 几乎不 增量垃圾收集 中 中 中 中 中 否 几乎不 增量保守垃圾收集 中 中 中 容易 高 否 几乎不
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期五, 六月 02, 2006
          
           -->
    Linker Script 链接器脚本
    http://blog.csdn.net/firststp/
    每个链接都由链接脚本控制着,脚本由链接器命令语言组成。脚本的主要目的是描述如何把输入文件中的节(sections)映射到输出文件中,并控制输出文件的存储布局。大多数的链接脚本就是做这些事情的,但在有必要时,脚本也可以指导链接器执行一些其他的操作。
    链接器总是使用链接器脚本,如果你没有提供一个你自己的脚本文件的话,编译器会使用一个缺省的脚本,而它被编译进链接器(?)。你可以使用"-verbose"命令行参数来显示缺省的链接脚本。而某些命令行选项,像"-r","-N"会影响缺省的链接脚本。
    在命令行选项中,通过参数"-T"你可以提供你自己的链接器脚本,这样就会替换缺省的脚本了。
    你还可以隐式地使用脚本,只要给一个脚本文件命名,并作为输入文件提交给链接器,就像是它们都是要被链接的文件一样。具体的内容清查看第11节。以下是目录:
    1 Basic Linker Script Concepts
    2 Linker Script Format
    3 Simple Linker Script Example
    4 Simple Linker Script Commands
    5 Assigning Values to Symbols
    6 SECTIONS command
    7 MEMORY command
    8 PHDRS Command
    9 VERSION Command
    10 Expressions in Linker Scripts
    11 Implicit Linker Scripts
    1 基本的链接器脚本的概念
    这里我们会定义一些基本的概念和一些词汇,来描述链接器脚本语言。

    接器把一些输入文件联合在一起,生成输出文件。输出的文件和输入文件都是object文件格式,每个文件都被称为对象文件(object
    file),而且,输出文件还经常被称为可执行文件。但这里我们依然称之为对象文件。每个对象文件在其中都包含有一个节(section)列表,我们有时
    称输入文件中的节(section)为输入节(input section),同样,输出文件中的节称为输出节(output section)。

    象文件中的每一个节都有名字和大小。大多数的节还有一个相连的数据块,就是有名的"section
    contents"。一个被标记为可加载(loadable)的节,意味着在输出文件运行时,contents可以被加载到内存中。没有contents
    的节也可以被加载,实际上处了一个数组被设置外,没有其他的东西被加载(在一些情况下,存储器必须被清0)。而既不是可加载的又不是可分配的
    (allocatable)节,通常包含了某些调试信息。
    每个可加载或可分配的输出节(output
    section)都有2个地址。第一个是虚拟存储地址VMA(virtual memory
    address),这是在输出文件执行时该节所使用的地址。第二个是加载存储地址LMA(load memory
    address),这是该节被加载是的地址。在大多数情况下,这两个地址是相同的。有个例子说明不同时的情况:当一个数据节(data
    section)加载在ROM中,后来在程序开始执行时又拷贝到RAM中(在基于ROM的系统中,这种技术经常用在初始化全局变量中)。在这种基于ROM
    的系统情况下,这时,ROM地址是LMA,而内存地址是VMA。
    要查看一个对象文件中各个节,可以使用objdump,并使用"-h"参数。

    个对象文件也有一个符号(symbles)列表,这就是著名的符合表(symble
    table)。一个符号可以是"已定义"(defined)或"无定义"(undefined)的。每个符号有名字,并且每个定义了的符号还有地址。在你
    编译一个c/c++程序成对象文件时,每个定义的函数,全局变量,静态变量,都可以有一个"已定义"的符号。输入文件中引用的每个没有定义的函数和全局变
    量则变成"无定义"的符号。
    使用nm可以查看对象文件中的符合,objdump并使用"-t"选项也可以。
    2 链接器脚本格式
    链接器脚本是一个文本文件。
    链接器脚本是一个命令序列,每个命令是一个关键字,可能还带着参数,又或者是对一个符号的赋值。你可以使用分号来隔开命令,而空格则通常被忽略。
    像文件名,格式名等字符串通常直接输入,如果文件名包含有像用于分割文件名的逗号等有其他用处的字符的话,你可以用双引号把文件名括起来。当然没有办法在文件名中使用双引号了。
    你可以使用注释,就像在C中,定界符是"/*"和"*/",和C中一样,注释在语法上等同于空格。
    3 简单的脚本例子
    很多的了解脚本都比较简单。可能最简单的链接器脚本只有一个命令: 'SECTIONS'。使用'SECTIONS'命令描述输出文件的内存布局。
    'SECTIONS'
    命令功能强大。这里描述一个简单的使用。我们假设你的只有代码(code),初始数据(initialized
    data)和未初始化的数据(uninitialized data)。它们要分别被放到'.text', '.data',
    '.bss'节中。更进一步假定它们是输入文件中的所有的节。
    这个例子中,代码要加载到地址0x10000,数据要从地址0x8000000开始。链接脚本如下:
    SECTIONS
    {
      . = 0x10000;
      .text : { *(.text) }
      . = 0x8000000;
      .data : { *(.data) }
      .bss : { *(.bss) }
    }
    'SECTIONS'命令的关键字是'SECTIONS',接着是一系列的符号(symbol)赋值,输出节(output section)描述被大括号包括着。

    面例子中,在'SECTIONS'命令里面,第一行设置一个值到一个特殊的符号'.',它是位置计数器(location
    counter),(像程序计数器PC)。如果你没有以某种其他的方式指定输出节(output
    section)的地址,地址就会是位置计数器中设置的当前值。而后,位置计数器就会以输出节的大小增加其值。在'SECTIONS'命令的开始,位置计
    数器是0。
    第2行定义'.text'输出节。冒号是必须的语法。在大括号里,输出节名字之后,你要列出要输入节的名字,它们会放入输出节中。通配符"*"匹配任何文件名,表达式"*(.text)"意味着所有的输入文件中的输入节".text"。
    因为在输出节".text"定义时,位置计数器是0x10000,所以链接器会设置输出文件中的".text"节的地址为0x10000。

    下的行定义输出文件中的".data"和".bss"节。链接器会把输出节".data"放置到地址0x8000000。之后,链接器把输出节
    ".data"的大小加到位置计数器的值0x8000000,
    并立即设置".bss"输出节,效果是在内存中,".bss"节会紧随".data"之后。
    链接器会确保每个输出节都有必要的对齐,它会在需要是增加位置计数器的值。在上面的例子中,指定的".text"和".data"节的地址都是符合对齐条件的,但是链接器可能会在".data"和".bss"间生成一个小间隙。
    4 简单的链接器脚本命令
    这里我们会描述简单的链接器脚本命令
    4.1 Setting the entry point
    4.2 Commands dealing with files
    4.3 Commands dealing with object file formats
    4.4 Other linker script commands
    4.1 设置入口点
    在一个程序中第一个指令称为入口点(entry point)。可以使用ENTRY链接器脚本命令来设置入口点。参数是一个符号名。
    ENTRY(symbol)
    有几种不同的方式来设置入口点。链接器会依次用下面的方法尝试设置入口点,当遇到成功时则停止。
      命令行选项"-e"  entry
      脚本中的"ENTRY(symbol)"
      如果有定义"start"符号,则使用start符号(symbol)。
      如果存在".text"节,则使用第一个字节的地址。
      地址0。
    4.2 处理文件的命令
    有一些处理文件的命令:
      INCLUDE filename
    在该点包含名称filename的链接脚本文件。文件会在当前路径下进行搜索,还有通过选项"-L"指定的目录。可以进行嵌套包含,你可以最多嵌套10层。
      INPUT(file,file,…)  或  INPUR(file file …)
    INPUT命令指示链接器在链接中包含指定的文件,好像它们命名在命令行上一样。
    例如,如果你总是要在链接时包含"subr.o",但是你又不想很烦地每次在命令行上输入,你可以在你的链接脚本中使用"INPUT(subr.o)"。
    实际上,如果你喜欢,你可以在链接脚本中列出所有的输入文件,然后使用选项"-T"来调用链接器,而不用做其他的。
    链接器首先尝试打开当前目录下的文件,如果没有,就通过存档库搜索路径进行搜索。你可以查看"-L"选项说明。
    如果你使用`INPUT(-l file)`,ld会把它转化成libfile.a,就想在命令行中使用"-l"参数。
    当你在一个隐式链接脚本中使用"INPUT"命令时,在链接器脚本文件被包含的点,文件会被包含进去。这会影响文档(archive)的搜索。
      GROUP( file, file, …)     GROUP(file file …)
    GROUP命令类似于INPUT命令,处了其文件为文档archive外。它们会被重复搜索,知道没有新的无定义(undefined)引用被创建。请查看"-("参数的描述。
      OUTPUT(filename)
    该命令指定输出文件的名字。相当于命令行中的`-o filename`参数。如果都使用了,命令行选项会优先。
    可以用OUTPUT命令来定义一个缺省的输出文件名,而不是无用的缺省`a.out`。
      SEARCH_DIR(path)
    此命令添加路径path到ld搜索库文档achive的路径列表中。相当于命令行方式下的`-L path`。如果都设置了,则都添加到列表中,而且,命令行中的在前,优先搜索。
      STARTUP(filename)
    此命令和INPUT命令相似,处了filename会成为第一个被链接的输入文件外,就想在命令行上被第一个输入。如果处理入口点总是第一个文件的开始,在这样的系统中,这会很有用。
    4.3 处理对象文件(object file)格式的命令
    处理对象文件格式的命令只有2个。
      OUTPUT_FORMAT(bfdname)   OUTPUT_FORMAT(default, big, little)
    此命令为用户的输出文件命名BFD 格式。相当于命令行中使用选项`--oformat bfdname`,如果都使用了,命令行方式优先。
    可以使用3个参数的OUTPUT_FORMAT指令,指定使用的不同的格式,就像命令行方式下的选项`-EB`, `-EL`。这样允许链接器脚本设置输出格式是指定的endianness编码。
    如果没有使用`-EB`或`-EL`指定endianness,输出格式会是第一个参数default。如果使用`-EB`,那么使用第二个参数big,使用了`-EL`,则使用参数little。
    例如,目标MIPS ELF 使用的缺省链接脚本使用命令:
    OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)
    这就是说,输出的缺省格式是elf32-bigmips,但是如果用户在命令行指定了选项`-EL`,则使用elf32-littlemips。
      TARGET(bfdname)

    读取输入文件时,此命令为用户命名BFD格式。它会影响随后的INPUT和GROUP命令。此命令相当于命令行方式的选项`-b bfdname`。
    如果使用了TARGET命令,而没有使用OUTPUT_FORMAR,那么最后的TARGET命令也设置输出文件的格式。
    4.4 其它的链接器脚本命令
    有一些其他的脚本命令:
      ASSERT(exp, message)
    确保表达式exp非零。如果为0,则退出链接,返回错误码,打印指定的消息message。
      EXTERN(symbol symbol …)
    强制要进入输出文件的指定的符号成为无定义undifined的符号。这么做,可以触发从标志库对附加模块的链接。可以列出多个符号symbol。对每个EXTERN,你可以使用EXTERN多次。这个命令和命令行下的选项`-u`产生一样的效果。
      FORCE_COMMON_ALLOCATION
    此命令的效果和命令行的选项`-d`一样,让ld 分配空间给公共common的符号,即使通过选项`-r`指定的是一个重定位的输出文件。
      INHIBIT_COMMON_ALLOCATION
    此命令和命令行选项`--no-define-common`有同样效果:让ld忽略对公共符号的赋值,即使是一个非可重定俍的(non-relocatable)输出文件
      NOCROSSREFS(section section …)
    此命令也许可以用来告诉ld在指定的输出节中对任何的引用发出一个错误。
    在某些特别类型的程序中,特别是嵌入式系统,如果使用覆盖图overlays,当一个节加载到内存中,而另一个节不在。这两个节间的任何方向的引用都会出错,例如,一个节中的代码调用另一个节中的函数。
    NOCROSSREFS命令带有一个输出节名字列表。如果ld测试在这些节之间有任何的交叉引用,就会报告一个错误,并返回一个非0的状态。
    注意,此指令使用的时输出节,而不是输入节。
      OUTPUT_ARCH(bfdarch)
    指定一个特定的机器架构输出。参数是BFD库中使用的名字。可以使用objdump 指定参数选项`-f`来查看架构。如ARM
    5 为符号指定值
    在脚本中,可以为一个符号symbol指定一个值。这样会把符号定义为全局符号symbol。
    5.1 Simple Assignments
    5.2 PROVIDE
    5.1 简单赋值
    使用任何的C赋值操作来给一个符号赋值。像下面这样:
    symbol = expression ;
    symbol += expression ;
    symbol -= expression ;
    symbol *= expression ;
    symbol /= expression ;
    symbol >= expression ;
    symbol &= expression ;
    symbol |= expression ;
    第一个例子中,定义了一个符号symbol,并赋值为expression。其他的例子中,symbol必须已被定义,根据操作调整其值。
    特殊的符号名"."指示的是位置计数器location counter。你只有在SECTIONS命令中才可以使用。
    表达式后的";"是必须的。你可以像命令一样按它们的顺序写符号赋值,或者在SECTIONS命令中像一个语句一样。或者作为SECTIONS命令中输出节描述器的一部分。
    例子:
    floating_point = 0;
    SECTIONS
    {
      .text :
        {
          *(.text)
          _etext = .;
        }
      _bdata = (. + 3) & ~ 3;
      .data : { *(.data) }
    }
    在例子中,符号`floating_point`会被定义为0。符号_etext会被定义为最近的输入节.text地址。符号_bdata定义为接着.text输出节的地址,但对齐到了4自己的边界。
    5.2 PROVIDE

    一些情况下,链接器脚本想要定义一个符号,这个符号仅仅被引用但没有被任何链接器中包含的对象定义。例如,传统的链接器定义符号"etext"作为一个函
    数名,而不会遇到错误。PROVIDE关键字可以用来定义像这样的符号,仅仅在它被引用却没有定义时。语法是:PROVIDE(symbol=
    expression)。
    下面是使用PROVIDE定义"etext"的例子:
    SECTIONS
    {
      .text :
        {
          *(.text)
          _etext = .;
          PROVIDE(etext = .);
        }
    }
    在例子中,如果程序定义了"_etext"(有下划线),链接器会给出多个定义的错误。另一方面,如果程序定义"etext"(没有下划线),链接器会在程序中静静地使用这个定义。如果程序引用"etext"但没有定义它,链接器会使用脚本中的定义。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    eCos linker script
    eCos linker script(ldi文件)中,文件由两部分组成;
    MEMORY
    {
    rom : ORIGIN = 0x40000000, LENGTH = 0x80000
    ram : ORIGIN = 0x48000000, LENGTH = 0x200000
    }
    MEMORY 块包含每一个内存区域的ORIGIN(起始地址)和LENGTH(长度);
    SECTIONS
    {
    SECTIONS_BEGIN
    SECTION_rom_vectors (rom, 0x40000000, LMA_EQ_VMA)
    SECTION_text (rom, ALIGN (0x1), LMA_EQ_VMA)
    SECTION_fini (rom, ALIGN (0x1), LMA_EQ_VMA)
    SECTION_rodata (rom, ALIGN (0x1), LMA_EQ_VMA)
    SECTION_rodata1 (rom, ALIGN (0x1), LMA_EQ_VMA)
    SECTION_fixup (rom, ALIGN (0x1), LMA_EQ_VMA)
    SECTION_gcc_except_table (rom, ALIGN (0x1), LMA_EQ_VMA)
    SECTION_data (ram, 0x48000000, FOLLOWING (.gcc_except_table))
    SECTION_bss (ram, ALIGN (0x4), LMA_EQ_VMA)
    SECTIONS_END
    }
    MEMORY块后面紧跟的是SECTIONS块,它包含了每一个连接器输出段的排列描述;每个SECTIONS是通过一个宏调用来描述的;
    这些宏的规则:
    1、 section块最后存放在哪个MEMORY区域;
    2、 section的最后地址(VMA);利用下列窗体之一进行表示:
    n            位于无符号整数n指定的绝对地址;
    ALIGN (n)      在之前section 的最后位置,用 n-byte边界对其;
    3、 section的起始地址(LMA);利用下列窗体之一进行表示:
    LMA_EQ_VMA      LMA等于VMA,不变换位置;
    AT (n)                位于无符号整数n指定的绝对地址;
    FOLLOWING (.name) 在section名字初始位置之后;
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    一个动态内存管理模块的实现
    北京恒基伟业电子产品有限公司 徐 文 来自:单片机与嵌入式系统应用
    摘要:介绍一个动态内存管理模块,可以有效地检测C程序中内存泄漏和写内存越界等错误,适用于具有标准C语言开发环境的各种平台。
        关键词:C语言 动态内存 内存泄漏 写越界
    引言

    前,绝大多数嵌入式平台上的软件都采用C语言编写。除了代码简洁、运行高效之外,灵活操作内存的能力更是C语言的重要特色。然而,不恰当的内存操作通常也
    是错误的根源之一。如“内存泄漏”
    ——不能正确地释放已分配的动态内存,就是一种非常难于检测的存错误。持续的内存泄漏会使程序性能下降到最终完全不能运行,进而影响到所有其它有动态内存
    需求的程序,在某些相对简单的嵌入式平台上甚至会妨碍操作系统的运转。再如“写内存越界”,一种不合法的写内存操作,极可能破坏到本程序中正在使用的其它
    数据,严重的时候还可能对其它正在运行的程序甚至整个系统造成影响。为此,本文介绍一个增强的、可定制的动态内存管理模块(以下不妨简称Fense),在
    C语言提供的内存分配函数基础上,增加了对动态内存的管理功能;能记录软件运行过程中出现的内存泄漏信息,同时也具一定的监测内存操作的能力;可以发现绝
    大多数对动态内存的写越界错误。
    1 Fense的设计原理
    Fense
    通过设立一个双向链表(struct Head
    *stHead)来保存所有被分配的动态内存块的信息。链表中的每个节点对应一个动态内存块,节点中包括此内存大小、分配发生时所在的源文件名和行号以及
    被释放的时候,Fense又从st_Head中删除之,检查st_Head中的节点即可得到未被释放的本节点的数值校验和等。Fense将每一个分配的动
    态内存块插入到链表st_Head中;当此内存放内存块信息。链表节点结构定义如下:
    struct Head{
    char file; /分配所在源文件名*/
    unsigned long line; /*分配所在的行号*/
    size_t size; /*分配的内存大小*/
    int checksum; /*链表节点校验和*/
    struct Head prev,next; /*双链表的前后节点指针*/
    };
    /*全局的双向链表*/
    struct Head *st_Head=NULL;
    为了检测写越界的错误,Fense在用户申请的内存前后各增加了一定大小的内存作为监测区域,并初始化成预定值。这样,当程序发生越界写操作时,预定值就会发生改变,Fense即可检测到错误。
    通过Fense分配到的动态内存结构如图1所示。由此可知,Fense_Malloc(Fense的内存分配函数)返回给用户的指针ptr指向的是用户申请内存区域的起始位置。链表节点、前/后监测区域均为Fense内部使用,是用户不可见的。

    2 用户定制选项
    Fense有5组宏定义提供给用户对功能进行定制。各组选项控制意义如下:
    WARN_ON_ZERO_MALLOC 用户申请零分配空间时警告信息。
    FILL_ON_MALLOC 分配时初始化内存块
    FILL_ON_MALLOC_VAL 分配初始化时的预设值
    FILL_ON_FREE 释放时填充内存块
    FILL_ON_FREE_VAL 释放时填充内存块的预设值
    以上4个选项的主要功能是初始化刚分配到的内存和刚被释放的内存为预设值,尽可能地避免出现因使用未初始经的内存而引发的错误。
    FENSE_FRONT_SIZE 定义前监测区域大小
    FENSE_FRONT_VAL 定义前监测区域的预设值
    FENSE_END_SIZE 定义后监测区域大小
    FENSE_END_VAL 定义后监测工域的预设值
    在Fense
    工作过程中,对内存越界写操作的检验是通过比较监测区域的当前值与本监测区域的预设值来确定的。显然不能排除这样一种可能:即发生在监测区域的越界写操作
    写入的数值与监测区域的预设值恰好相同,此时,Fense无法发现错误的发生。对于这种情况,用户可以通过更改监测区域预设值
    (FENSE_FRONT_VAL和FENSE_END_VAL)和监测区域大小(FENSE_FRONT_SIZE和FENSE_END_SIZE)为
    多组不同的值来反复测试,这样就可以大幅度地提高监测的准确性。
    VALIDATE_FREE
    free是检查本内存块是否在链表中
    CHECK_ALL_MEMORY_ON_FREE
    free时检查链表中的所有内存块
    由于存在这样一种情况:对内存块A的写操作出现了越界错误,写到了另一内存块B的区域内。此时,仅仅检查内存块A的有效性就无法发现问题,如果同时检查所有的动态内存块,则有可能发现错误所在。以上选项即为此而设。
    FENSE_LOCK 获取对链表st_Head的操作权
    FENSE_UNLOCK 释放对链表st_Head的操作权

    虑到的在多线程环境中,可能有多个线程同时用Fense进行内存管理,而Fense使用的链表st_Head是全局变量,因此提供了以上2个宏来实现对
    st_Head的互斥访问。宏的具体定义依赖于用户所在的软件环境,用户可自行实现。对于单线程系统,仅需将这2个宏定义为空即可。
    为便于使用,Fense的头文件中还包括了以下定义,使得用户基本不用改动现有的源代码就可引入Fense。
    #define malloc(size) Fense_Malloc(size,_FILE_,_LINE_)
    #define free(ptr) Fense_Free(ptr,_FILE_,_LINE_)
    #define realloc(ptr,new_size) Fense_Realloc(ptr,new_size,_FILE_,_LINE_)
    #define colloc(num,size) Fense_Calloc(num,size,_FILE_,_LINE_)
    3 运行时控制
    Fense
    监测内存的功能可以在运行动态地开关。此功能通过将全局变量st_Disbaled赋值为零或非零来实现。在调试过程中,可以在调试器中即时修改
    st_Disabled的值来控制Fense的行为,省去了重编译源代码的需要。对于那些需要大量编译时间的大型工程或交叉平台开发的软件项目来说,这是
    非常有利的。
    4 Fense的具体实现
    Fense
    提供Fense_Malloc、Fense_Free、Fense_Realloc及Fense_Calloc等内存管理函数,功能和调用形式与C语言中
    的malloc、free、realloc和calloc保持一致。限于篇幅,这里仅对Fense_Malloc和Fense_Free的实现过程做一个
    简单描述,具体实现请见本刊网络补充版。http://www.dpj.com.cn
    /*内存分配函数*/
    void *Fense_Malloc(size_t size,char *file,unsigned long line)
    {
    //检查Fense的运行时开关,如果Fense被关闭,则调用malloc
    //分配并返回
    //检查是否零分配,如有则提示警告信息后返回0(用户定制选项)
    //分配内存,包括链表节点区域和前/后监测区域
    //初始化链表节点,保存分配内存的信息,包括分配的大小、所在文件名和行号
    //将此节点插入链表st_Head
    //为本节点区域计算校验和
    //用预设值初始化前/后监测区域
    //用预设值填充用户内存区域(用户定制选项)
    //返回用户内存区域的起始位置
    }
    /*内存释放函数*/
    void Fense_Free(void *uptr,char *file,unsigned long line)
    {
    //检查Fense的运行时开关,如果Fense初关闭,则调用free释译并返回
    //检查所有Fense管理下的动态内存(用户定制选项)
    //判断当前内存块是否在链表st_Head中,如果不在则提示
    //警靠信息,退出(用户定制选项)
    //检查当前内存块是否存在越界操作
    //将当前内存块的相应的链表节点从st_Head中删除
    //重新计算当前节点的前后相邻节点的校验和
    //用预设值填充被释放的内存区(用户定制选项)
    //调用free释放当前的内存块
    }
    (文中代码在Visual C++6.0、Borland C++ 3.1及CrossCode C 7.4环境中编译通过)
    结束语
    作为对C程序运行时的内存错误进行监测的代码模块,Fense能发现几乎所有的内存泄漏和绝大多数的越界操作,并尽可能地记录了改正程序错误所需要的信息;有效地减少了程序设计人员的调试时间,在实际嵌入式产品开发中取得了很好的效果。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    C/C++程序内存泄漏检测


    http://www.cppblog.com/edog/archive/2006/02/15/3268.html

    1. 包含头文件和定义:#define _CRTDBG_MAP_ALLOC
    #include
    #include
    如果定义了_CRTDBG_MAP_ALLOC,打印出来的是文件名和行数等更加直观的信息。

    2. 方法一
    在程序入口写几个语句:
    int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
    tmpFlag |= _CRTDBG_LEAK_CHECK_DF;
    _CrtSetDbgFlag( tmpFlag );
    程序退出时,如果发现有内存泄漏,会自动在DEBUG OUTPUT窗口和DebugView中输出内存泄漏信息。
    3. 方法二在程序任何地方用以下语句:
    _CrtDumpMemoryLeaks();
    随时检测打印内存泄漏信息,如果有的话。不过此用法有个局限,对于一些全局函数,如果初始化时申请了内存,到程序结束时候才释放,此函数会一直把新申请的内存当作泄漏来对待。
    4. 方法三
    使用_CrtMemCheckpoint方法,在某段程序中统计泄漏信息。如下:
    _CrtMemState s1, s2, s3;
    _CrtMemCheckpoint( &s1 );

    // 程序段1:DO SOMETHING
    _CrtMemCheckpoint( &s2 );
    if ( _CrtMemDifference( &s3, &s1, &s2) )
      _CrtMemDumpStatistics( &s3 );
    可以统计程序段1中是否发生了内存泄漏。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    How To Find Memory Leaks

    http://www.flipcode.com/articles/article_memoryleaks.shtml

    Introduction


    I
    was recently working on a rather large project, the largest I had ever
    been involved with. We didn't have a concrete design document on this
    project so ideas and implementations were constantly changing. This is
    a great flexibility from a design/creation standpoint, but from a
    programming perspective it ended up becoming a rather large mess. Deep
    into the project we realized that with release date approaching we
    should tackle the task of cleaning up the garbage code and stabilizing
    it for alpha testing. Being a small company however meant that programs
    like BoundsChecker?from NuMega were just not within our reach. So we
    had to improvise. We needed a drop in solution to our current code base
    (the code base was entirely C++). We didn't want a project specific fix
    however. We needed a simple solution that could easily be compiled into
    this project and any other. Then at the end of runtime it would
    generate a list of the un-freed memory blocks. Well, of course we found
    it, otherwise this article would not be. We'll be right back after
    these messages to bring you the solution!
    We're back. Let me
    elaborate more on exactly what we needed in our "memory tracer". We
    first needed something that could be added to any existing code base.
    Code reuse is a very important consideration, especially to a company.
    It can potentially save hundreds of hours and thousands of dollars.
    Secondly, our solution had to be simple. We didn't have time nor the
    courage to wade through thousands of lines of code doing re-writes and
    fixes to accommodate our memory tracer. And finally, it had to be free.
    So we took a look at our code. The first thing we noticed is
    that nearly all of our memory allocations were accomplished through the
    operator new and variants. And likewise the de-allocations were
    accomplished through delete and variants. Well, we could replace all
    occurrences of new and delete with proprietary functions that tracked
    our memory for us? No! Too many replacements! C++ allows you to
    override new and delete in your classes. That would be great news if it
    didn't mean adding these overrides to all our classes. Wait a minute...
    I can override the global new and delete operators!!! Now I can do
    whatever just before each memory allocation and de-allocation! This
    great news! Sort of. I had actually known about this in a roundabout
    way. You see, MFC also has this ability. It is exploited through some
    of the _Crt() functions. Now that we have our back door, all we need to
    do is to track the allocations and cross-reference them with the
    de-allocations at destruction time. What doesn't get referenced is a
    leak. Simple.
    Lets get to the workings of it shall we. All my
    work and references are written using Visual C++. It should be trivial
    to convert to other vendors. The first thing to do is to override new
    and delete so that they will be overridden everywhere in the program.
    In stdafx.h, I add:
    [color="#000000"]
          #ifdef _DEBUG
          [color="#0000ff"]inline [color="#0000ff"]void * __cdecl [color="#0000ff"]operator [color="#0000ff"]new([color="#0000ff"]unsigned [color="#0000ff"]int size,
                                             [color="#0000ff"]const [color="#0000ff"]char *file, [color="#0000ff"]int line)
          {
          };
          [color="#0000ff"]inline [color="#0000ff"]void __cdecl [color="#0000ff"]operator [color="#0000ff"]delete([color="#0000ff"]void *p)
          {
          };
          [color="#0000ff"]#endif

    These
    are my overridden functions. And by encasing them with #ifdef/#endif
    quotes, I don't get sub-par code with Release builds. When you look at
    the code you'll notice that new has been overridden with three
    parameters. These are the size of the requested allocation as well as
    the file and line from the source file where the allocation takes
    place. This is necessary in finding where the leaks are. Otherwise they
    would take a lot of assembly digging to find. By adding this however,
    all our code base that calls new() still refers to the operator new
    function that accepts one parameter, and not our three parameter new
    function. In addition, we wouldn't want to recode all our new operator
    statements to include the __FILE__ and __LINE__ arguments. What we need
    to do is to automatically make the one-parameter new operator call a
    three-parameter new operator call. This can be accomplished with some
    macro trickery.
    [color="#000000"]
          #ifdef _DEBUG
          [color="#0000ff"]#define DEBUG_NEW [color="#0000ff"]new(__FILE__, __LINE__)
          [color="#0000ff"]#else
          [color="#0000ff"]#define DEBUG_NEW [color="#0000ff"]new
          [color="#0000ff"]#endif
          [color="#0000ff"]#define [color="#0000ff"]new DEBUG_NEW

    Now
    all of our one-parameter new operator calls are three parameter new
    calls with the __FILE__ and __LINE__ automatically inserted by the
    pre-compiler. Now is time for the actual tracking. We should also add
    the memory routines to our overridden functions so that they at least
    do what the old new/delete operator functions did.
    [color="#000000"]
          #ifdef _DEBUG
          [color="#0000ff"]inline [color="#0000ff"]void * __cdecl [color="#0000ff"]operator [color="#0000ff"]new([color="#0000ff"]unsigned [color="#0000ff"]int size,
                                             [color="#0000ff"]const [color="#0000ff"]char *file, [color="#0000ff"]int line)
          {
                  [color="#0000ff"]void *ptr = ([color="#0000ff"]void *)malloc(size);
                  AddTrack((DWORD)ptr, size, file, line);
                  [color="#0000ff"]return(ptr);
          };
          [color="#0000ff"]inline [color="#0000ff"]void __cdecl [color="#0000ff"]operator [color="#0000ff"]delete([color="#0000ff"]void *p)
          {
                  RemoveTrack((DWORD)p);
                  free(p);
          };
          [color="#0000ff"]#endif

    In
    addition to these, you may also need to override the new[] and delete[]
    operators as well. They are the same so I just left them out to save
    space.
    Finally we need to supply some code the tracking
    functions AddTrack() and RemoveTrack(). I use the STL to maintain my
    linked list of allocations. You can use whatever. They two functions
    can contain any code you wish and then some. But I'll provide you with
    my version just in case you have coders block.
    [color="#000000"]
          typedef [color="#0000ff"]struct {
                  DWORD        address;
                  DWORD        size;
                  [color="#0000ff"]char        file[64];
                  DWORD        line;
          } ALLOC_INFO;
          [color="#0000ff"]typedef list AllocList;
          AllocList *allocList;
          [color="#0000ff"]void AddTrack(DWORD addr,  DWORD asize,  [color="#0000ff"]const [color="#0000ff"]char *fname, DWORD lnum)
          {
                  ALLOC_INFO *info;
                  [color="#0000ff"]if(!allocList) {
                          allocList = [color="#0000ff"]new(AllocList);
                  }
                  info = [color="#0000ff"]new(ALLOC_INFO);
                  info->address = addr;
                  strncpy(info->file, fname, 63);
                  info->line = lnum;
                  info->size = asize;
                  allocList->insert(allocList->begin(), info);
          };
          [color="#0000ff"]void RemoveTrack(DWORD addr)
          {
                  AllocList::iterator i;
                  [color="#0000ff"]if(!allocList)
                          [color="#0000ff"]return;
                  [color="#0000ff"]for(i = allocList->begin(); i != allocList->end(); i++)
                  {
                          [color="#0000ff"]if((*i)->address == addr)
                          {
                                  allocList->remove((*i));
                                  [color="#0000ff"]break;
                          }
                  }
          };

    Now,
    at the very last moment before our program exits, allocList is a list
    of all memory allocations that have not been freed. But in order to see
    what and where they are, you need to dump the information stored within
    allocList. I use the output window in Visual C++ for this. You can
    format this information in any way but what I've provided just dumps
    the list of information. Note that using the debug output window of
    Visual C++ may result in some lines of text not getting outputted
    before it cuts off.
    [color="#000000"]
          void DumpUnfreed()
          {
                  AllocList::iterator i;
                  DWORD totalSize = 0;
                  [color="#0000ff"]char buf[1024];
                  [color="#0000ff"]if(!allocList)
                          [color="#0000ff"]return;
                  [color="#0000ff"]for(i = allocList->begin(); i != allocList->end(); i++) {
                          sprintf(buf, "%-50s:ttLINE %d,ttADDRESS %dt%d unfreedn",
                                  (*i)->file, (*i)->line, (*i)->address, (*i)->size);
                          OutputDebugString(buf);
                          totalSize += (*i)->size;
                  }
                  sprintf(buf, "-----------------------------------------------------------n");
                  OutputDebugString(buf);
                  sprintf(buf, "Total Unfreed: %d bytesn", totalSize);
                  OutputDebugString(buf);
          };

    There
    you have it. A bit of reusable code that you can use in all your
    projects to track all your memory leaks. I always add these functions
    to every project I start working on and I've used them to clean up
    projects already completed. It may not help make your game look the
    best, but it will hopefully help stabilize it. Take care everyone and
    send me any and all comments.
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    程序写作上的三十二个“修养”
    本文转自:榭伍德森林 URL:
    http://p0prxx.yculblog.com/post.1095675.html
    我总结了在用C/C++语言(主要是C语言)进行程序写作上的三十二个“修养”,通过这些,你可以写出质量高的程序,同时也会让看你程序的人渍渍称道,那些看过你程序的人一定会说:“这个人的编程修养不错”。
      ------------------------
        
        01、版权和版本
        02、缩进、空格、换行、空行、对齐
        03、程序注释
        04、函数的[in][out]参数
        05、对系统调用的返回进行判断
        06、if 语句对出错的处理
        07、头文件中的#ifndef
        08、在堆上分配内存
        09、变量的初始化
        10、h和c文件的使用
        11、出错信息的处理
        12、常用函数和循环语句中的被计算量
        13、函数名和变量名的命名
        14、函数的传值和传指针
        15、修改别人程序的修养
        16、把相同或近乎相同的代码形成函数和宏
        17、表达式中的括号
        18、函数参数中的const
        19、函数的参数个数
        20、函数的返回类型,不要省略
        21、goto语句的使用
        22、宏的使用
        23、static的使用
        24、函数中的代码尺寸
        25、typedef的使用
        26、为常量声明宏
        27、不要为宏定义加分号
        28、||和&&的语句执行顺序
        29、尽量用for而不是while做循环
        30、请sizeof类型而不是变量
        31、不要忽略Warning
        32、书写Debug版和Release版的程序
      ------------------------
    1、版权和版本
    -------
    好的程序员会给自己的每个函数,每个文件,都注上版权和版本。
    对于C/C++的文件,文件头应该有类似这样的注释:
    /************************************************************************
    *
    *  文件名:network.c
    *
    *  文件描述:网络通讯函数集
    *
    *  创建人: Hao Chen, 2003年2月3日
    *
    *  版本号:1.0
    *
    *  修改记录:
    *
    ************************************************************************/
    而对于函数来说,应该也有类似于这样的注释:
    /*================================================================
    *
    * 函 数 名:XXX
    *
    * 参  数:
    *
    *    type name [IN] : descripts
    *
    * 功能描述:
    *
    *    ..............
    *
    * 返 回 值:成功TRUE,失败FALSE
    *
    * 抛出异常:
    *
    * 作  者:ChenHao 2003/4/2
    *
    ================================================================*/
    这样的描述可以让人对一个函数,一个文件有一个总体的认识,对代码的易读性和易维护性有很大的好处。这是好的作品产生的开始。
    2、缩进、空格、换行、空行、对齐
    ----------------
    i)
    缩进应该是每个程序都会做的,只要学程序过程序就应该知道这个,但是我仍然看过不缩进的程序,或是乱缩进的程序,如果你的公司还有写程序不缩进的程序员,
    请毫不犹豫的开除他吧,并以破坏源码罪起诉他,还要他赔偿读过他程序的人的精神损失费。缩进,这是不成文规矩,我再重提一下吧,一个缩进一般是一个TAB
    键或是4个空格。(最好用TAB键)
    ii) 空格。空格能给程序代来什么损失吗?没有,有效的利用空格可以让你的程序读进来更加赏心悦目。而不一堆表达式挤在一起。看看下面的代码:
      ha=(ha*128+*key++)%tabPtr->size;
      ha = ( ha * 128 + *key++ ) % tabPtr->size;
      有空格和没有空格的感觉不一样吧。一般来说,语句中要在各个操作符间加空格,函数调用时,要以各个参数间加空格。如下面这种加空格的和不加的:
      
    if ((hProc=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid))==NULL){
    }
    if ( ( hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) ) == NULL ){
    }
    iii) 换行。不要把语句都写在一行上,这样很不好。如:
      for(i=0;i´9´)&&(a´z´)) break;
      
      我拷,这种即无空格,又无换行的程序在写什么啊?加上空格和换行吧。  
      
      for ( i=0; i    if ( ( a  ´9´ ) &&
           ( a  ´z´ ) ) {
          break;
        }
      }
      好多了吧?有时候,函数参数多的时候,最好也换行,如:
      CreateProcess(
             NULL,
             cmdbuf,
             NULL,
             NULL,
             bInhH,
             dwCrtFlags,
             envbuf,
             NULL,
             &siStartInfo,
             &prInfo
             );
      条件语句也应该在必要时换行:
      
      if ( ch >= ´0´ || ch = ´a´ || ch = ´A´ || ch name;
    }
    不!请不要这样做。

    应该先判断一下传进来的那个指针是不是为空。如果传进来的指针为空的话,那么,你的一个大的系统就会因为这一个小的函数而崩溃。一种更好的技术是使用断
    言(assert),这里我就不多说这些技术细节了。当然,如果是在C++中,引用要比指针好得多,但你也需要对各个参数进行检查。
    写有参数的函数时,首要工作,就是要对传进来的所有参数进行合法性检查。而对于传出的参数也应该进行检查,这个动作当然应该在函数的外部,也就是说,调用完一个函数后,应该对其传出的值进行检查。
    当然,检查会浪费一点时间,但为了整个系统不至于出现“非法操作”或是“Core Dump”的系统级的错误,多花这点时间还是很值得的。
    5、对系统调用的返回进行判断
    --------------
    继续上一条,对于一些系统调用,比如打开文件,我经常看到,许多程序员对fopen返回的指针不做任何判断,就直接使用了。然后发现文件的内容怎么也读出不,或是怎么也写不进去。还是判断一下吧:
      fp = fopen("log.txt", "a");
      if ( fp == NULL ){
        printf("Error: open file errorn");
        return FALSE;
      }
    其它还有许多啦,比如:socket返回的socket号,malloc返回的内存。请对这些系统调用返回的东西进行判断。
    6、if 语句对出错的处理
    -----------
    我看见你说了,这有什么好说的。还是先看一段程序代码吧。
      if ( ch >= ´0´ && ch  ´9´ ){
        /* 输出错误信息 */
        printf("error ......n");
        return ( FALSE );
      }
      
      /* 正常处理代码 */
      ......
    这样的结构,不是很清楚吗?突出了错误的条件,让别人在使用你的函数的时候,第一眼就能看到不合法的条件,于是就会更下意识的避免。
    7、头文件中的#ifndef
    ----------
    千万不要忽略了头件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突。
    还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:
      #ifndef 
      #define  
      
      ......
      ......
      
      #endif
      
    在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
      #ifndef _STDIO_H_
      #define _STDIO_H_
      
      ......
      
      #endif
      
    (BTW:预编译有多很有用的功能。你会用预编译吗?)  
      
    8、在堆上分配内存
    ---------

    能许多人对内存分配上的“栈 stack”和“堆
    heap”还不是很明白。包括一些科班出身的人也不明白这两个概念。我不想过多的说这两个东西。简单的来讲,stack上分配的内存系统自动释放,
    heap上分配的内存,系统不释放,哪怕程序退出,那一块内存还是在那里。stack一般是静态分配内存,heap上一般是动态分配内存。
    由malloc
    系统函数分配的内存就是从堆上分配内存。从堆上分配的内存一定要自己释放。用free释放,不然就是术语--“内存泄露”(或是“内存漏 洞”)--
    Memory Leak。于是,系统的可分配内存会随malloc越来越少,直到系统崩溃。还是来看看“栈内存”和“堆内存”的差别吧。
      栈内存分配
      -----
      char*
      AllocStrFromStack()
      {
        char pstr[100];
        return pstr;
      }
      
      
      堆内存分配
      -----
      char*
      AllocStrFromHeap(int len)
      {
        char *pstr;
        
        if ( len  ErrCode, ServerListener -> ServLisner,UserAccount -> UsrAcct 等。
      5) 为了避免全局函数和变量名字冲突,可以加上一些前缀,一般以模块简称做为前缀。
      6) 全局变量统一加一个前缀或是后缀,让人一看到这个变量就知道是全局的。
      7) 用匈牙利命名法命名函数参数,局部变量。但还是要坚持“望文生意”的原则。
      8) 与标准库(如:STL)或开发库(如:MFC)的命名风格保持一致。
      
      
    14、函数的传值和传指针
    ------------
    向函数传参数时,一般而言,传入非const的指针时,就表示,在函数中要修改这个指针把指内存中的数据。如果是传值,那么无论在函数内部怎么修改这个值,也影响不到传过来的值,因为传值是只内存拷贝。
    什么?你说这个特性你明白了,好吧,让我们看看下面的这个例程:
    void
    GetVersion(char* pStr)
    {
      pStr = malloc(10);
      strcpy ( pStr, "2.0" );
    }
    main()
    {
      char* ver = NULL;
      GetVersion ( ver );
      ...
      ...
      free ( ver );
    }
    我保证,类似这样的问题是一个新手最容易犯的错误。程序中妄图通过函数GetVersion给指针ver分配空间,但这种方法根本没有什么作用,原因就是--这是传值,不是传指针。你或许会和我争论,我分明传的时指针啊?再仔细看看,其实,你传的是指针其实是在传值。
    15、修改别人程序的修养
    -----------

    你维护别人的程序时,请不要非常主观臆断的把已有的程序删除或是修改。我经常看到有的程序员直接在别人的程序上修改表达式或是语句。修改别人的程序时,
    请不要删除别人的程序,如果你觉得别人的程序有所不妥,请注释掉,然后添加自己的处理程序,必竟,你不可能100%的知道别人的意图,所以为了可以恢复,
    请不依赖于CVS或是SourceSafe这种版本控制软件,还是要在源码上给别人看到你修改程序的意图和步骤。这是程序维护时,一个有修养的程序员所应
    该做的。
    如下所示,这就是一种比较好的修改方法:
      /*
       * ----- commented by haoel 2003/04/12 ------
       *
       *  char* p = ( char* ) malloc( 10 );
       *  memset( p, 0, 10 );
       */
      
      /* ------ Added by haoel  2003/04/12 ----- */
       char* p = ( char* )calloc( 10, sizeof char );
      /* ---------------------------------------- */
      ...
    当然,这种方法是在软件维护时使用的,这样的方法,可以让再维护的人很容易知道以前的代码更改的动作和意图,而且这也是对原作者的一种尊敬。
    以“注释 - 添加”方式修改别人的程序,要好于直接删除别人的程序。
    16、把相同或近乎相同的代码形成函数和宏
    ---------------------
    有人说,最好的程序员,就是最喜欢“偷懒”的程序,其中不无道理。
    如果你有一些程序的代码片段很相似,或直接就是一样的,请把他们放在一个函数中。而如果这段代码不多,而且会被经常使用,你还想避免函数调用的开销,那么就把他写成宏吧。
    千万不要让同一份代码或是功能相似的代码在多个地方存在,不然如果功能一变,你就要修改好几处地方,这种会给维护带来巨大的麻烦,所以,做到“一改百改”,还是要形成函数或是宏。
    17、表达式中的括号
    ---------
    如果一个比较复杂的表达式中,你并不是很清楚各个操作符的忧先级,即使是你很清楚优先级,也请加上括号,不然,别人或是自己下一次读程序时,一不小心就看走眼理解错了,为了避免这种“误解”,还有让自己的程序更为清淅,还是加上括号吧。
    比如,对一个结构的成员取地址:
      GetUserAge( &( UserInfo->age ) );
    虽然,&UserInfo->age中,->操作符的优先级最高,但加上一个括号,会让人一眼就看明白你的代码是什么意思。
    再比如,一个很长的条件判断:
    if ( ( ch[0] >= ´0´ || ch[0] = ´a´ || ch[1] = ´A´ || ch[2] b?a:b

    我们这样使用宏时,没有什么问题: MAX( num1, num2 ); 因为宏展开后变成
    num1>num2?num1:num2;。但是,如果是这样调用的,MAX( 17+32, 25+21 );
    呢,编译时出现错误,原因是,宏展开后变成:17+32>25+21?17+32:25+21,哇,这是什么啊?
    所以,宏在使用时,参数一定要加上括号,上述的那个例子改成如下所示就能解决问题了。
      #define MAX( (a), (b) )   (a)>(b)?(a):(b)
      
    即使是这样,也不这个宏也还是有Bug,因为如果我这样调用 MAX(i++, j++); ,经过这个宏以后,i和j都被累加了两次,这绝不是我们想要的。
      

    以,在宏的使用上还是要谨慎考虑,因为宏展开是的结果是很难让人预料的。而且虽然,宏的执行很快(因为没有函数调用的开销),但宏会让源代码澎涨,使目
    标文件尺寸变大,(如:一个50行的宏,程序中有1000个地方用到,宏展开后会很不得了),相反不能让程序执行得更快(因为执行文件变大,运行时系统换
    页频繁)。
    因此,在决定是用函数,还是用宏时得要小心。
    23、static的使用
    --------
    static关键字,表示了“静态”,一般来说,他会被经常用于变量和函数。一个static的变量,其实就是全局变量,只不过他是有作用域的全局变量。比如一个函数中的static变量:
    char*
    getConsumerName()
    {
      static int cnt = 0;
      
      ....
      cnt++;
      ....
    }
    cnt变量的值会跟随着函数的调用次而递增,函数退出后,cnt的值还存在,只是cnt只能在函数中才能被访问。而cnt的内存也只会在函数第一次被调用时才会被分配和初始化,以后每次进入函数,都不为static分配了,而直接使用上一次的值。
    对于一些被经常调用的函数内的常量,最好也声明成static(参见第12条)
    但static
    的最多的用处却不在这里,其最大的作用的控制访问,在C中如果一个函数或是一个全局变量被声明为static,那么,这个函数和这个全局变
    量,将只能在这个C文件中被访问,如果别的C文件中调用这个C文件中的函数,或是使用其中的全局(用extern关键字),将会发生链接时错误。这个特性
    可以用于数据和程序保密。
    24、函数中的代码尺寸
    ----------
    一个函数完成一个具体的功能,一般来说,
    一个函数中的代码最好不要超过600行左右,越少越好,最好的函数一般在100行以内,300行左右的孙函数就差
    不多了。有证据表明,一个函数中的代码如果超过500行,就会有和别的函数相同或是相近的代码,也就是说,就可以再写另一个函数。
    另外,函数一般是完成一个特定的功能,千万忌讳在一个函数中做许多件不同的事。函数的功能越单一越好,一方面有利于函数的易读性,另一方面更有利于代码的维护和重用,功能越单一表示这个函数就越可能给更多的程序提供服务,也就是说共性就越多。
    虽然函数的调用会有一定的开销,但比起软件后期维护来说,增加一些运行时的开销而换来更好的可维护性和代码重用性,是很值得的一件事。
    25、typedef的使用
    ---------
    typedef是一个给类型起别名的关键字。不要小看了它,它对于你代码的维护会有很好的作用。比如C中没有bool,于是在一个软件中,一些程序员使用int,一些程序员使用short,会比较混乱,最好就是用一个typedef来定义,如:
      typedef char bool;
      
    一般来说,一个C的工程中一定要做一些这方面的工作,因为你会涉及到跨平台,不同的平台会有不同的字长,所以利用预编译和typedef可以让你最有效的维护你的代码,如下所示:
      #ifdef SOLARIS2_5
       typedef boolean_t   BOOL_T;
      #else
       typedef int      BOOL_T;
      #endif
      
      typedef short      INT16_T;
      typedef unsigned short UINT16_T;
      typedef int       INT32_T;
      typedef unsigned int  UINT32_T;
      
      #ifdef WIN32
       typedef _int64    INT64_T;
      #else
       typedef long long   INT64_T;
      #endif
      
      typedef float      FLOAT32_T;
      typedef char*      STRING_T;
      typedef unsigned char  BYTE_T;
      typedef time_t     TIME_T;
      typedef INT32_T     PID_T;
      
    使用typedef的其它规范是,在结构和函数指针时,也最好用typedef,这也有利于程序的易读和可维护性。如:
      typedef struct _hostinfo {
        HOSTID_T  host;
        INT32_T  hostId;
        STRING_T  hostType;
        STRING_T  hostModel;
        FLOAT32_T cpuFactor;
        INT32_T  numCPUs;
        INT32_T  nDisks;
        INT32_T  memory;
        INT32_T  swap;
      } HostInfo;
      typedef INT32_T (*RsrcReqHandler)(
       void *info,
       JobArray *jobs,
       AllocInfo *allocInfo,
       AllocList *allocList);
    C++中这样也是很让人易读的:
      typedef CArray HostInfoArray;
    于是,当我们用其定义变量时,会显得十分易读。如:
      HostInfo* phinfo;
      RsrcReqHandler* pRsrcHand;
    这种方式的易读性,在函数的参数中十分明显。
    关键是在程序种使用typedef后,几乎所有的程序中的类型声明都显得那么简洁和清淅,而且易于维护,这才是typedef的关键。
    26、为常量声明宏
    --------
    最好不要在程序中出现数字式的“硬编码”,如:
      int user[120];
      
    为这个120声明一个宏吧。为所有出现在程序中的这样的常量都声明一个宏吧。比如TimeOut的时间,最大的用户数量,还有其它,只要是常量就应该声明成宏。如果,突然在程序中出现下面一段代码,
      for ( i=0; i    ....
      }
    这样就很容易了解这段程序的意图了。
    有的程序员喜欢为这种变量声明全局变量,其实,全局变量应该尽量的少用,全局变量不利于封装,也不利于维护,而且对程序执行空间有一定的开销,一不小心就造成系统换页,造成程序执行速度效率等问题。所以声明成宏,即可以免去全局变量的开销,也会有速度上的优势。
    27、不要为宏定义加分号
    -----------
    有许多程序员不知道在宏定义时是否要加分号,有时,他们以为宏是一条语句,应该要加分号,这就错了。当你知道了宏的原理,你会赞同我为会么不要为宏定义加分号的。看一个例子:
      #define MAXNUM 1024;
    这是一个有分号的宏,如果我们这样使用:
      half = MAXNUM/2;
      
      if ( num 0 ) { PRINT_LINE; }
      
    都不要在最后加上分号,当我们在程序中使用时,为之加上分号,
      main()
      {
        char *p = LINE;
        PRINT_LINE;
      }
    这一点非常符合习惯,而且,如果忘加了分号,编译器给出的错误提示,也会让我们很容易看懂的。
    28、||和&&的语句执行顺序
    ------------
    条件语句中的这两个“与”和“或”操作符一定要小心,它们的表现可能和你想像的不一样,这里条件语句中的有些行为需要和说一下:
      express1 || express2
        
      先执行表达式express1如果为“真”,express2将不被执行,express2仅在express1为“假”时才被执行。因为第一个表达式为真了,整个表达式都为真,所以没有必要再去执行第二个表达式了。
      express1 && express2
      先执行表达式express1如果为“假”,express2将不被执行,express2仅在express1为“真”时才被执行。因为第一个表达式为假了,整个表达式都为假了,所以没有必要再去执行第二个表达式了。
    于是,他并不是你所想像的所有的表达式都会去执行,这点一定要明白,不然你的程序会出现一些莫明的运行时错误。
    例如,下面的程序:
      if ( sum > 100 &&
         ( ( fp=fopen( filename,"a" ) ) != NULL )  {
        
         fprintf(fp, "Warring: it beyond one hundredn");
         ......
      }
      
      fprintf( fp, " sum is %id n", sum );
      fclose( fp );
    本来的意图是,如果sum > 100 ,向文件中写一条出错信息,为了方便,把两个条件判断写在一起,于是,如果sumnext;
      }
    当while的语句块变大后,你的程序将很难读,用for就好得多:
      for ( p=pHead; p; p=p->next ){
      ..
      }
    一眼就知道这个循环的开始条件,结束条件,和循环的推进。大约就能明白这个循环要做个什么事?而且,程序维护进来很容易,不必像while一样,在一个编辑器中上上下下的捣腾。
    30、请sizeof类型而不是变量
    -------------
    许多程序员在使用sizeof中,喜欢sizeof变量名,例如:
    int score[100];
    char filename[20];
    struct UserInfo usr[100];
    在sizeof这三个的变量名时,都会返回正确的结果,于是许多程序员就开始sizeof变量名。这个习惯很虽然没有什么不好,但我还是建议sizeof类型。
    我看到过这个的程序:
      pScore = (int*) malloc( SUBJECT_CNT );
      memset( pScore, 0, sizeof(pScore) );
      ...
      
    此时,sizeof(pScore)返回的就是4(指针的长度),不会是整个数组,于是,memset就不能对这块内存进行初始化。为了程序的易读和易维护,我强烈建议使用类型而不是变量,如:
    对于score:   sizeof(int) * 100  /* 100个int */
    对于filename: sizeof(char) * 20  /* 20个char */
    对于usr:    sizeof(struct UserInfo) * 100  /* 100个UserInfo */
    这样的代码是不是很易读?一眼看上去就知道什么意思了。
    另外一点,sizeof一般用于分配内存,这个特性特别在多维数组时,就能体现出其优点了。如,给一个字符串数组分配内存,
    /*
    * 分配一个有20个字符串,
    * 每个字符串长100的内存
    */
    char* *p;
    /*
    * 错误的分配方法
    */
    p = (char**)calloc( 20*100, sizeof(char) );
    /*
    * 正确的分配方法
    */
    p = (char**) calloc ( 20, sizeof(char*) );
    for ( i=0; i
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期四, 六月 01, 2006
          
           -->
    如何在linux下检测内存泄漏
    http://publishblog.blogchina.com/blog/tb.b?diaryID=2681354

    想检测内存泄漏,必须对程序中的内存分配和释放情况进行记录,所能够采取的办法有重载所有形式的operator new 和 operator
    delete,截获 new operator 和 delete operator 执行过程中的内存操作信息。下面列出的就是重载形式
    void* operator new( size_t nSize, char* pszFileName, int nLineNum )
    void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )
    void operator delete( void *ptr )
    void operator delete[]( void *ptr )

    们为 operator new 定义了一个新的版本,除了必须的 size_t nSize
    参数外,还增加了文件名和行号,这里的文件名和行号就是这次 new operator
    操作符被调用时所在的文件名和行号,这个信息将在发现内存泄漏时输出,以帮助用户定位泄漏具体位置。对于 operator
    delete,因为无法为之定义新的版本,我们直接覆盖了全局的 operator delete 的两个版本。
    在重载的 operator new 函数版本中,我们将调用全局的 operator new 的相应的版本并将相应的 size_t 参数传入,而后,我们将全局 operator new 返回的指针值以及该次分配所在的文件名和行号信息记录下来,这里所采用的数据结构是一个 STL 的 map,
    以指针值为 key 值。当 operator delete
    被调用时,如果调用方式正确的话(调用方式不正确的情况将在后面详细描述),我们就能以传入的指针值在 map
    中找到相应的数据项并将之删除,而后调用 free 将指针所指向的内存块释放。当程序退出的时候,map
    中的剩余的数据项就是我们企图检测的内存泄漏信息--已经在堆上分配但是尚未释放的分配信息。
    以上就是内存检测实现的基本原理,现在还有两个基本问题没有解决:
    1) 如何取得内存分配代码所在的文件名和行号,并让 new operator 将之传递给我们重载的 operator new。
    2) 我们何时创建用于存储内存数据的 map 数据结构,如何管理,何时打印内存泄漏信息。
    先解决问题1。首先我们可以利用 C 的预编译宏 __FILE__ 和 __LINE__,这两个宏将在编译时在指定位置展开为该文件的文件名和该行的行号。而后我们需要将缺省的全局 new operator 替换为我们自定义的能够传入文件名和行号的版本,我们在子系统头文件 MemRecord.h 中定义:
    #define DEBUG_NEW new(__FILE__, __LINE__ )
    而后在所有需要使用内存检测的客户程序的所有的 cpp 文件的开头加入
    #include "MemRecord.h"
    #define new DEBUG_NEW

    可以将客户源文件中的对于全局缺省的 new operator 的调用替换为 new (__FILE__,__LINE__)
    调用,而该形式的new operator将调用我们的operator new (size_t nSize, char*
    pszFileName, int nLineNum),其中 nSize 是由 new operator 计算并传入的,而 new
    调用点的文件名和行号是由我们自定义版本的 new operator
    传入的。我们建议在所有用户自己的源代码文件中都加入上述宏,如果有的文件中使用内存检测子系统而有的没有,则子系统将可能因无法监控整个系统而输出一些
    泄漏警告。
    再说第二个问题。我们用于管理客户
    信息的这个 map 必须在客户程序第一次调用 new operator 或者 delete operator 之前被创建,而且在最后一个
    new operator 和 delete operator
    调用之后进行泄漏信息的打印,也就是说它需要先于客户程序而出生,而在客户程序退出之后进行分析。能够包容客户程序生命周期的确有一人--全局对象
    (appMemory)。我们可以设计一个类来封装这个 map
    以及这对它的插入删除操作,然后构造这个类的一个全局对象(appMemory),在全局对象(appMemory)的构造函数中创建并初始化这个数据结
    构,而在其析构函数中对数据结构中剩余数据进行分析和输出。Operator new 中将调用这个全局对象(appMemory)的 insert
    接口将指针、文件名、行号、内存块大小等信息以指针值为 key 记录到 map 中,在 operator delete 中调用 erase
    接口将对应指针值的 map 中的数据项删除,注意不要忘了对 map 的访问需要进行互斥同步,因为同一时间可能会有多个线程进行堆上的内存操作。

    啦,内存检测的基本功能已经具备了。但是不要忘了,我们为了检测内存泄漏,在全局的 operator new
    增加了一层间接性,同时为了保证对数据结构的安全访问增加了互斥,这些都会降低程序运行的效率。因此我们需要让用户能够方便的 enable 和
    disable
    这个内存检测功能,毕竟内存泄漏的检测应该在程序的调试和测试阶段完成。我们可以使用条件编译的特性,在用户被检测文件中使用如下宏定义:
    #include "MemRecord.h"
    #if defined( MEM_DEBUG )
    #define new DEBUG_NEW
    #endif
    当用户需要使用内存检测时,可以使用如下命令对被检测文件进行编译
    g++ -c -DMEM_DEBUG xxxxxx.cpp
    就可以 enable 内存检测功能,而用户程序正式发布时,可以去掉 -DMEM_DEBUG 编译开关来 disable 内存检测功能,消除内存检测带来的效率影响。
    图2所示为使用内存检测功能后,内存泄漏代码的执行以及检测结果

    图2
    4.错误方式删除带来的问题
    以上我们已经构建了一个具备基本内存泄漏检测功能的子系统,下面让我们来看一下关于内存泄漏方面的一些稍微高级一点的话题。

    先,在我们编制 c++ 应用时,有时需要在堆上创建单个对象,有时则需要创建对象的数组。关于 new 和 delete
    原理的叙述我们可以知道,对于单个对象和对象数组来说,内存分配和删除的动作是大不相同的,我们应该总是正确的使用彼此搭配的 new 和
    delete 形式。但是在某些情况下,我们很容易犯错误,比如如下代码:
                    class Test {};
                    ……
                    Test* pAry = new Test[10];//创建了一个拥有 10 个 Test 对象的数组
                    Test* pObj = new Test;//创建了一个单对象
                    ……
                    delete []pObj;//本应使用单对象形式 delete pObj 进行内存释放,却错误的使用了数
    //组形式
                    delete pAry;//本应使用数组形式 delete []pAry 进行内存释放,却错误的使用了单对
    //象的形式

    匹配的 new 和 delete 会导致什么问题呢?C++
    标准对此的解答是"未定义",就是说没有人向你保证会发生什么,但是有一点可以肯定:大多不是好事情--在某些编译器形成的代码中,程序可能会崩溃,而另
    外一些编译器形成的代码中,程序运行可能毫无问题,但是可能导致内存泄漏。

    然知道形式不匹配的 new 和 delete 会带来的问题,我们就需要对这种现象进行毫不留情的揭露,毕竟我们重载了所有形式的内存操作
    operator new,operator new[],operator delete,operator delete[]。

    们首先想到的是,当用户调用特定方式(单对象或者数组方式)的 operator new
    来分配内存时,我们可以在指向该内存的指针相关的数据结构中,增加一项用于描述其分配方式。当用户调用不同形式的 operator delete
    的时候,我们在 map 中找到与该指针相对应的数据结构,然后比较分配方式和释放方式是否匹配,匹配则在 map
    中正常删除该数据结构,不匹配则将该数据结构转移到一个所谓 "ErrorDelete" 的 list
    中,在程序最终退出的时候和内存泄漏信息一起打印。

    面这种方法是最顺理成章的,但是在实际应用中效果却不好。原因有两个,第一个原因我们上面已经提到了:当 new 和 delete
    形式不匹配时,其结果"未定义"。如果我们运气实在太差--程序在执行不匹配的 delete
    时崩溃了,我们的全局对象(appMemory)中存储的数据也将不复存在,不会打印出任何信息。第二个原因与编译器相关,前面提到过,当编译器处理自定
    义数据类型或者自定义数据类型数组的 new 和 delete 操作符的时候,通常使用编译器相关的 cookie 技术。这种 cookie
    技术在编译器中可能的实现方式是:new operator 先计算容纳所有对象所需的内存大小,而后再加上它为记录 cookie
    所需要的内存量,再将总容量传给operator new 进行内存分配。当 operator new 返回所需的内存块后,new
    operator 将在调用相应次数的构造函数初始化有效数据的同时,记录 cookie
    信息。而后将指向有效数据的指针返回给用户。也就是说我们重载的 operator new 所申请到并记录下来的指针与 new operator
    返回给调用者的指针不一定一致(图3)。当调用者将 new operator 返回的指针传给 delete operator
    进行内存释放时,如果其调用形式相匹配,则相应形式的 delete operator
    会作出相反的处理,即调用相应次数的析构函数,再通过指向有效数据的指针位置找出包含 cookie 的整块内存地址,并将其传给 operator
    delete 释放内存。如果调用形式不匹配,delete operator
    就不会做上述运算,而直接将指向有效数据的指针(而不是真正指向整块内存的指针)传入 operator delete。因为我们在 operator
    new 中记录的是我们所分配的整块内存的指针,而现在传入 operator delete
    的却不是,所以就无法在全局对象(appMemory)所记录的数据中找到相应的内存分配信息。


    图3

    上所述,当 new 和 delete 的调用形式不匹配时,由于程序有可能崩溃或者内存子系统找不到相应的内存分配信息,在程序最终打印出
    "ErrorDelete"
    的方式只能检测到某些"幸运"的不匹配现象。但我们总得做点儿什么,不能让这种危害极大的错误从我们眼前溜走,既然不能秋后算帐,我们就实时输出一个
    warning 信息来提醒用户。什么时候抛出一个 warning 呢?很简单,当我们发现在 operator delete 或
    operator delete[] 被调用的时候,我们无法在全局对象(appMemory)的 map
    中找到与传入的指针值相对应的内存分配信息,我们就认为应该提醒用户。

    然决定要输出warning信息,那么现在的问题就是:我们如何描述我们的warning信息才能更便于用户定位到不匹配删除错误呢?答案:在
    warning 信息中打印本次 delete 调用的文件名和行号信息。这可有点困难了,因为对于 operator delete 我们不能向对象
    operator new 一样做出一个带附加信息的重载版本,我们只能在保持其接口原貌的情况下,重新定义其实现,所以我们的 operator
    delete 中能够得到的输入只有指针值。在 new/delete 调用形式不匹配的情况下,我们很有可能无法在全局对象(appMemory)的
    map 中找到原来的 new
    调用的分配信息。怎么办呢?万不得已,只好使用全局变量了。我们在检测子系统的实现文件中定义了两个全局变量(DELETE_FILE,
    DELETE_LINE)记录 operator delete 被调用时的文件名和行号,同时为了保证并发的 delete
    操作对这两个变量访问同步,还使用了一个 mutex(至于为什么是 CCommonMutex 而不是一个
    pthread_mutex_t,在"实现上的问题"一节会详细论述,在这里它的作用就是一个 mutex)。
    char DELETE_FILE[ FILENAME_LENGTH ] = {0};
    int DELETE_LINE = 0;
    CCommonMutex globalLock;
    而后,在我们的检测子系统的头文件中定义了如下形式的 DEBUG_DELETE
                            extern char DELETE_FILE[ FILENAME_LENGTH ];
    extern int DELETE_LINE;
    extern CCommonMutex globalLock;//在后面解释
    #define DEBUG_DELETE         globalLock.Lock();
                            if (DELETE_LINE != 0) BuildStack(); (//见第六节解释)
                            strncpy( DELETE_FILE, __FILE__,FILENAME_LENGTH - 1 );
                            DELETE_FILE[ FILENAME_LENGTH - 1 ]= '';
                            DELETE_LINE = __LINE__;
                            delete
                           
    在用户被检测文件中原来的宏定义中添加一条:
    #include "MemRecord.h"
    #if defined( MEM_DEBUG )
    #define new DEBUG_NEW
    #define delete DEBUG_DELETE
    #endif

    样,在用户被检测文件调用 delete operator
    之前,将先获得互斥锁,然后使用调用点文件名和行号对相应的全局变量(DELETE_FILE,DELETE_LINE)进行赋值,而后调用
    delete operator。当 delete operator 最终调用我们定义的 operator delete
    的时候,在获得此次调用的文件名和行号信息后,对文件名和行号全局变量(DELETE_FILE,DELETE_LINE)重新初始化并打开互斥锁,让下
    一个挂在互斥锁上的 delete operator 得以执行。
    在对 delete operator 作出如上修改以后,当我们发现无法经由 delete operator 传入的指针找到对应的内存分配信息的时候,就打印包括该次调用的文件名和行号的 warning。
    天下没有十全十美的事情,既然我们提供了一种针对错误方式删除的提醒方法,我们就需要考虑以下几种异常情况:
    1.
    用户使用的第三方库函数中有内存分配和释放操作。或者用户的被检测进程中进行内存分配和释放的实现文件没有使用我们的宏定义。由于我们替换了全局的
    operator delete,这种情况下的用户调用的 delete 也会被我们截获。用户并没有使用我们定义的DEBUG_NEW
    宏,所以我们无法在我们的全局对象(appMemory)数据结构中找到对应的内存分配信息,但是由于它也没有使用DEBUG_DELETE,我们为
    delete 定义的两个全局 DELETE_FILE 和 DELETE_LINE 都不会有值,因此可以不打印 warning。
    2.
    用户的一个实现文件调用了 new 进行内存分配工作,但是该文件并没有使用我们定义的 DEBUG_NEW
    宏。同时用户的另一个实现文件中的代码负责调用 delete 来删除前者分配的内存,但不巧的是,这个文件使用了 DEBUG_DELETE
    宏。这种情况下内存检测子系统会报告 warning,并打印出 delete 调用的文件名和行号。
    3.
    与第二种情况相反,用户的一个实现文件调用了 new 进行内存分配工作,并使用我们定义的 DEBUG_NEW
    宏。同时用户的另一个实现文件中的代码负责调用 delete 来删除前者分配的内存,但该文件没有使用 DEBUG_DELETE
    宏。这种情况下,因为我们能够找到这个内存分配的原始信息,所以不会打印 warning。
    4. 当出现嵌套 delete(定义可见"实现上的问题")的情况下,以上第一和第三种情况都有可能打印出不正确的 warning 信息,详细分析可见"实现上的问题"一节。
    你可能觉得这样的 warning 太随意了,有误导之嫌。怎么说呢?作为一个检测子系统,对待有可能的错误我们所采取的原则是:宁可误报,不可漏报。请大家"有则改之,无则加勉"。
    5.动态内存泄漏信息的检测

    面我们所讲述的内存泄漏的检测能够在程序整个生命周期结束时,打印出在程序运行过程中已经在堆上分配但是没有释放的内存分配信息,程序员可以由此找到程序
    中"显式"的内存泄漏点并加以改正。但是如果程序在结束之前能够将自己所分配的所有内存都释放掉,是不是就可以说这个程序不存在内存泄漏呢?答案:否!在
    编程实践中,我们发现了另外两种危害性更大的"隐式"内存泄漏,其表现就是在程序退出时,没有任何内存泄漏的现象,但是在程序运行过程中,内存占用量却不
    断增加,直到使整个系统崩溃。
    1. 程序的一个线程不断分配内存,并将指向内存的指针保存在一个数据存储中(如 list),但是在程序运行过程中,一直没有任何线程进行内存释放。当程序退出的时候,该数据存储中的指针值所指向的内存块被依次释放。
    2.
    程序的N个线程进行内存分配,并将指针传递给一个数据存储,由M个线程从数据存储进行数据处理和内存释放。由于 N
    远大于M,或者M个线程数据处理的时间过长,导致内存分配的速度远大于内存被释放的速度。但是在程序退出的时候,数据存储中的指针值所指向的内存块被依次
    释放。
    之所以说他危害性更大,是因为很不容易这种问题找出来,程序可能连续运行几个十几个小时没有问题,从而通过了不严密的系统测试。但是如果在实际环境中 7×24 小时运行,系统将不定时的崩溃,而且崩溃的原因从 log 和程序表象上都查不出原因。
    为了将这种问题也挑落马下,我们增加了一个动态检测模块 MemSnapShot,用于在程序运行过程中,每隔一定的时间间隔就对程序当前的内存总使用情况和内存分配情况进行统计,以使用户能够对程序的动态内存分配状况进行监视。

    客户使用 MemSnapShot
    进程监视一个运行中的进程时,被监视进程的内存子系统将把内存分配和释放的信息实时传送给MemSnapShot。MemSnapShot
    则每隔一定的时间间隔就对所接收到的信息进行统计,计算该进程总的内存使用量,同时以调用new进行内存分配的文件名和行号为索引值,计算每个内存分配动
    作所分配而未释放的内存总量。这样一来,如果在连续多个时间间隔的统计结果中,如果某文件的某行所分配的内存总量不断增长而始终没有到达一个平衡点甚至回
    落,那它一定是我们上面所说到的两种问题之一。

    实现上,内存检测子系统的全局对象(appMemory)的构造函数中以自己的当前 PID 为基础 key
    值创建一个消息队列,并在operator new 和 operator delete
    被调用的时候将相应的信息写入消息队列。MemSnapShot 进程启动时需要输入被检测进程的 PID,而后通过该 PID 组装 key
    值并找到被检测进程创建的消息队列,并开始读入消息队列中的数据进行分析统计。当得到operator new 的信息时,记录内存分配信息,当收到
    operator delete
    消息时,删除相应的内存分配信息。同时启动一个分析线程,每隔一定的时间间隔就计算一下当前的以分配而尚未释放的内存信息,并以内存的分配位置为关键字进
    行统计,查看在同一位置(相同文件名和行号)所分配的内存总量和其占进程总内存量的百分比。
    图4 是一个正在运行的 MemSnapShot 程序,它所监视的进程的动态内存分配情况如图所示:


    图四

    支持 MemSnapShot
    过程中的实现上的唯一技巧是--对于被检测进程异常退出状况的处理。因为被检测进程中的内存检测子系统创建了用于进程间传输数据的消息队列,它是一个核心
    资源,其生命周期与内核相同,一旦创建,除非显式的进行删除或系统重启,否则将不被释放。

    错,我们可以在内存检测子系统中的全局对象(appMemory)的析构函数中完成对消息队列的删除,但是如果被检测进程非正常退出(CTRL+C,段错
    误崩溃等),消息队列可就没人管了。那么我们可以不可以在全局对象(appMemory)的构造函数中使用 signal 系统调用注册
    SIGINT,SIGSEGV
    等系统信号处理函数,并在处理函数中删除消息队列呢?还是不行,因为被检测进程完全有可能注册自己的对应的信号处理函数,这样就会替换我们的信号处理函
    数。最终我们采取的方法是利用 fork
    产生一个孤儿进程,并利用这个进程监视被检测进程的生存状况,如果被检测进程已经退出(无论正常退出还是异常退出),则试图删除被检测进程所创建的消息队
    列。下面简述其实现原理:
    在全局对象
    (appMemory)构造函数中,创建消息队列成功以后,我们调用 fork 创建一个子进程,而后该子进程再次调用 fork
    创建孙子进程,并退出,从而使孙子进程变为一个"孤儿"进程(之所以使用孤儿进程是因为我们需要切断被检测进程与我们创建的进程之间的信号联系)。孙子进
    程利用父进程(被检测进程)的全局对象(appMemory)得到其 PID 和刚刚创建的消息队列的标识,并传递给调用 exec
    函数产生的一个新的程序映象--MemCleaner。
    MemCleaner 程序仅仅调用 kill(pid, 0);函数来查看被检测进程的生存状态,如果被检测进程不存在了(正常或者异常退出),则 kill 函数返回非 0 值,此时我们就动手清除可能存在的消息队列。
    6.实现上的问题:嵌套delete
    在"
    错误方式删除带来的问题"一节中,我们对 delete operator
    动了个小手术--增加了两个全局变量(DELETE_FILE,DELETE_LINE)用于记录本次 delete
    操作所在的文件名和行号,并且为了同步对全局变量(DELETE_FILE,DELETE_LINE)的访问,增加了一个全局的互斥锁。在一开始,我们使
    用的是 pthread_mutex_t,但是在测试中,我们发现 pthread_mutex_t 在本应用环境中的局限性。
    例如如下代码:
                    class B {…};
                            class A {
                            public:
                                    A() {m_pB = NULL};
                                    A(B* pb) {m_pB = pb;};
                                    ~A()
                                    {
                                           if (m_pB != NULL)
               行号1                                        delete m_pB;                //这句最要命
                                     };
                            private:
                                    class B* m_pB;
                                    ……
                            }
                    int main()
                    {
                            A* pA = new A(new B);
                            ……
              行号2                delete pA;               
                    }
    在上述代码中,main 函数中的一句 delete pA 我们称之为"嵌套删除",即我们 delete A 对象的时候,在A对象的析构执行了另一个 delete B 的动作。当用户使用我们的内存检测子系统时,delete pA 的动作应转化为以下动作:
                    上全局锁
                    全局变量(DELETE_FILE,DELETE_LINE)赋值为文件名和行号2
                    delete operator A
                      调用~A()
                        上全局锁
                        全局变量(DELETE_FILE,DELETE_LINE)赋值为文件名和行号1
                        delete operator B
                          调用~B()
                          返回~B()
                          调用operator delete B
                            记录全局变量(DELETE_FILE,DELETE_LINE)值1并清除全局变量(DELETE_FILE,DELETE_LINE)值
                            打开全局锁
                        返回operator delete B
                    返回delete operator B
                 返回~A()
             调用 operator delete A
               记录全局变量(DELETE_FILE,DELETE_LINE)值1并清除全局变量(DELETE_FILE,DELETE_LINE)值
               打开全局锁
             返回operator delete A
          返回 delete operator A
           
    在这一过程中,有两个技术问题,一个是 mutex 的可重入问题,一个是嵌套删除时 对全局变量(DELETE_FILE,DELETE_LINE)现场保护的问题。
    所谓 mutex 的可重入问题,是指在同一个线程上下文中,连续对同一个 mutex 调用了多次 lock,然后连续调用了多次 unlock。这就是说我们的应用方式要求互斥锁有如下特性:
    1. 要求在同一个线程上下文中,能够多次持有同一个互斥体。并且只有在同一线程上下文中调用相同次数的 unlock 才能放弃对互斥体的占有。
    2. 对于不同线程上下文持有互斥体的企图,同一时间只有一个线程能够持有互斥体,并且只有在其释放互斥体之后,其他线程才能持有该互斥体。
    Pthread_mutex_t
    互斥体不具有以上特性,即使在同一上下文中,第二次调用 pthread_mutex_lock
    将会挂起。因此,我们必须实现出自己的互斥体。在这里我们使用 semaphore 的特性实现了一个符合上述特性描述的互斥体
    CCommonMutex(源代码见附件)。

    了支持特性 2,在这个 CCommonMutex 类中,封装了一个 semaphore,并在构造函数中令其资源值为 1,初始值为1。当调用
    CCommonMutex::lock 接口时,调用 sem_wait 得到 semaphore,使信号量的资源为 0 从而让其他调用 lock
    接口的线程挂起。当调用接口 CCommonMutex::unlock 时,调用 sem_post 使信号量资源恢复为
    1,让其他挂起的线程中的一个持有信号量。

    时为了支持特性 1,在这个 CCommonMutex 增加了对于当前线程 pid 的判断和当前线程访问计数。当线程第一次调用 lock
    接口时,我们调用 sem_wait 的同时,记录当前的 Pid 到成员变量 m_pid,并置访问计数为 1,同一线程(m_pid ==
    getpid())其后的多次调用将只进行计数而不挂起。当调用 unlock 接口时,如果计数不为 1,则只需递减访问计数,直到递减访问计数为
    1 才进行清除 pid、调用 sem_post。(具体代码可见附件)
    嵌套删除时对全局变量(DELETE_FILE,DELETE_LINE)
    场保护的问题是指,上述步骤中在 A 的析构函数中调用 delete m_pB
    时,对全局变量(DELETE_FILE,DELETE_LINE)文件名和行号的赋值将覆盖主程序中调用 delete pA
    时对全局变量(DELETE_FILE,DELETE_LINE)的赋值,造成了在执行 operator delete A 时,delete pA
    的信息全部丢失。
    要想对这些全局信息进行
    现场保护,最好用的就是堆栈了,在这里我们使用了 STL 提供的 stack 容器。在 DEBUG_DELETE
    宏定义中,对全局变量(DELETE_FILE,DELETE_LINE)赋值之前,我们先判断是否前面已经有人对他们赋过值了--观察行号变量是否等于
    0,如果不为 0,则应该将已有的信息压栈(调用一个全局函数 BuildStack()
    将当前的全局文件名和行号数据压入一个全局堆栈globalStack),而后再对全局变量(DELETE_FILE,DELETE_LINE)赋值,再
    调用 delete operator。而在内存子系统的全局对象(appMemory)提供的 erase 接口里面,如果判断传入的文件名和行号为
    0,则说明我们所需要的数据有可能被嵌套删除覆盖了,所以需要从堆栈中弹出相应的数据进行处理。

    在嵌套删除中的问题基本解决了,但是当嵌套删除与 "错误方式删除带来的问题"一节的最后所描述的第一和第三种异常情况同时出现的时候,由于用户的
    delete 调用没有通过我们定义的 DEBUG_DELETE 宏,上述机制可能出现问题。其根本原因是我们利用stack 保留了经由我们的
    DEBUG_DELETE 宏记录的 delete 信息的现场,以便在 operator delete 和全局对象(appMemory)的
    erase 接口中使用,但是用户的没经过 DEBUG_DELETE 宏的 delete 操作却未曾进行压栈操作而直接调用了 operator
    delete,有可能将不属于这次操作的 delete 信息弹出,破坏了堆栈信息的顺序和有效性。那么,当我们因为无法找到这次及其后续的
    delete 操作所对应的内存分配信息的时候,可能会打印出错误的 warning 信息。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期一, 五月 29, 2006
          
           -->
    内核空间和用户空间
    用户空间
      在Linux中,每个用户进程都可以访问4GB的线性虚拟内存空间。其中从0到3GB的虚存地址是用户空间,用户进程可以直接访问。
    内核空间
      从3GB到4GB的虚存地址为内核态空间,存放供内核访问的代码和数据,用户态进程不能访问。所有进程从3GB到4GB的虚拟空间都是一样的,linux以此方式让内核态进程共享代码段和数据段。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期三, 五月 24, 2006
          
           -->
    汇编语言的准备知识
    汇编语言和CPU以及内存,端口等硬件知识是连在一起的. 这也是为什么汇编语言没有通用性的原因. 下面简单讲讲基本知识(针对INTEL x86及其兼容机)
    ============================
    x86
    汇编语言的指令,其操作对象是CPU上的寄存器,系统内存,或者立即数. 有些指令表面上没有操作数, 或者看上去缺少操作数,
    其实该指令有内定的操作对象, 比如push指令, 一定是对SS:ESP指定的内存操作, 而cdq的操作对象一定是eax / edx.
    在汇编语言中,寄存器用名字来访问. CPU 寄存器有好几类, 分别有不同的用处:
    1. 通用寄存器:
    EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP(这个虽然通用,但很少被用做除了堆栈指针外的用途)

    些32位可以被用作多种用途,但每一个都有"专长". EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器.
    EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址. ECX 是计数器(counter),
    是重复(REP)前缀指令和LOOP指令的内定计数器. EDX是...(忘了..哈哈)但它总是被用来放整数除法产生的余数.
    这4个寄存器的低16位可以被单独访问,分别用AX,BX,CX和DX. AX又可以单独访问低8位(AL)和高8位(AH),
    BX,CX,DX也类似. 函数的返回值经常被放在EAX中.
    ESI/EDI分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
    EBP是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码:
    push ebp ;保存当前ebp
    mov ebp,esp ;EBP设为当前堆栈指针
    sub esp, xxx ;预留xxx字节给函数临时变量.
    ...
    这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可.
    ESP 专门用作堆栈指针.
    2. 段寄存器:
    CS(Code
    Segment,代码段) 指定当前执行的代码段. EIP (Instruction pointer, 指令指针)则指向该段中一个具体的指令.
    CS:EIP指向哪个指令, CPU 就执行它. 一般只能用jmp, ret, jnz, call 等指令来改变程序流程,而不能直接对它们赋值.
    DS(DATA SEGMENT, 数据段) 指定一个数据段. 注意:在当前的计算机系统中, 代码和数据没有本质差别,
    都是一串二进制数, 区别只在于你如何用它. 例如, CS 制定的段总是被用作代码, 一般不能通过CS指定的地址去修改该段.
    然而,你可以为同一个段申请一个数据段描述符"别名"而通过DS来访问/修改. 自修改代码的程序常如此做.
    ES,FS,GS 是辅助的段寄存器, 指定附加的数据段.
    SS(STACK SEGMENT)指定当前堆栈段. ESP 则指出该段中当前的堆栈顶. 所有push/pop 系列指令都只对SS:ESP指出的地址进行操作.
    3. 标志寄存器(EFLAGS):
    该寄存器有32位,组合了各个系统标志. EFLAGS一般不作为整体访问, 而只对单一的标志位感兴趣. 常用的标志有:
    进位标志C(CARRY), 在加法产生进位或减法有借位时置1, 否则为0.
    零标志Z(ZERO), 若运算结果为0则置1, 否则为0
    符号位S(SIGN), 若运算结果的最高位置1, 则该位也置1.
    溢出标志O(OVERFLOW), 若(带符号)运算结果超出可表示范围, 则置1.
    JXX
    系列指令就是根据这些标志来决定是否要跳转, 从而实现条件分枝. 要注意,很多JXX 指令是等价的, 对应相同的机器码. 例如, JE 和JZ
    是一样的,都是当Z=1是跳转. 只有JMP 是无条件跳转. JXX 指令分为两组, 分别用于无符号操作和带符号操作. JXX 后面的"XX"
    有如下字母:
    无符号操作: 带符号操作:
    A = "ABOVE", 表示"高于" G = "GREATER", 表示"大于"
    B = "BELOW", 表示"低于" L = "LESS", 表示"小于"
    C = "CARRY", 表示"进位"或"借位" O = "OVERFLOW", 表示"溢出"
    S = "SIGN", 表示"负"
    通用符号:
    E = "EQUAL" 表示"等于", 等价于Z (ZERO)
    N = "NOT" 表示"非", 即标志没有置位. 如JNZ "如果Z没有置位则跳转"
    Z = "ZERO", 与E同.
    如果仔细想一想,就会发现 JA = JNBE, JAE = JNB, JBE = JNA, JG = JNLE, JGE= JNL, JL= JNGE, ....
    4. 端口

    口是直接和外部设备通讯的地方。外设接入系统后,系统就会把外设的数据接口映射到特定的端口地址空间,这样,从该端口读入数据就是从外设读入数据,而向外
    设写入数据就是向端口写入数据。当然这一切都必须遵循外设的工作方式。端口的地址空间与内存地址空间无关,系统总共提供对64K个8位端口的访问,编号0
    -65535.
    相邻的8位端口可以组成成一个16位端口,相邻的16位端口可以组成一个32位端口。端口输入输出由指令IN,OUT,INS和OUTS实现,具体可参考
    汇编语言书籍。
    汇编指令的操作数可以是内存中的数据, 如何让程序从内存中正确取得所需要的数据就是对内存的寻址.
    INTEL 的CPU 可以工作在两种寻址模式:实模式和保护模式. 前者已经过时,就不讲了, WINDOWS 现在是32位保护模式的系统, PE 文件就基本是运行在一个32位线性地址空间, 所以这里就只介绍32位线性空间的寻址方式.

    实线性地址的概念是很直观的, 就想象一系列字节排成一长队,第一个字节编号为0, 第二个编号位1, ....
    一直到4294967295(十六进制FFFFFFFF,这是32位二进制数所能表达的最大值了). 这已经有4GB的容量!
    足够容纳一个程序所有的代码和数据. 当然, 这并不表示你的机器有那么多内存. 物理内存的管理和分配是很复杂的内容, 初学者不必在意, 总之,
    从程序本身的角度看, 就好象是在那么大的内存中.
    在INTEL系统中,
    内存地址总是由"段选择符:有效地址"的方式给出.段选择符(SELECTOR)存放在某一个段寄存器中, 有效地址则可由不同的方式给出.
    段选择符通过检索段描述符确定段的起始地址, 长度(又称段限制), 粒度, 存取权限, 访问性质等. 先不用深究这些,
    只要知道段选择符可以确定段的性质就行了. 一旦由选择符确定了段, 有效地址相对于段的基地址开始算. 比如由选择符1A7选择的数据段,
    其基地址是400000, 把1A7 装入DS中, 就确定使用该数据段. DS:0 就指向线性地址400000. DS:1F5278
    就指向线性地址5E5278. 我们在一般情况下, 看不到也不需要看到段的起始地址, 只需要关心在该段中的有效地址就行了. 在32位系统中,
    有效地址也是由32位数字表示, 就是说, 只要有一个段就足以涵盖4GB线性地址空间, 为什么还要有不同的段选择符呢? 正如前面所说的,
    这是为了对数据进行不同性质的访问. 非法的访问将产生异常中断, 而这正是保护模式的核心内容, 是构造优先级和多任务系统的基础.
    这里有涉及到很多深层的东西, 初学者先可不必理会.
    有效地址的计算方式是: 基址+间址*比例因子+偏移量. 这些量都是指段内的相对于段起始地址的量度, 和段的起始地址没有关系. 比如, 基址=100000, 间址=400, 比例因子=4, 偏移量=20000, 则有效地址为:
    100000+400*4+20000=100000+1000+20000=121000. 对应的线性地址是400000+121000=521000. (注意, 都是十六进制数).

    址可以放在任何32位通用寄存器中, 间址也可以放在除ESP外的任何一个通用寄存器中. 比例因子可以是1, 2, 4 或8. 偏移量是立即数.
    如: [EBP+EDX*8+200]就是一个有效的有效地址表达式. 当然, 多数情况下用不着这么复杂, 间址,比例因子和偏移量不一定要出现.

    存的基本单位是字节(BYTE). 每个字节是8个二进制位, 所以每个字节能表示的最大的数是11111111, 即十进制的255. 一般来说,
    用十六进制比较方便, 因为每4个二进制位刚好等于1个十六进制位, 11111111b = 0xFF. 内存中的字节是连续存放的,
    两个字节构成一个字(WORD), 两个字构成一个双字(DWORD). 在INTEL架构中, 采用small endian格式,
    即在内存中,高位字节在低位字节后面. 举例说明:十六进制数803E7D0C, 每两位是一个字节, 在内存中的形式是: 0C 7D 3E 80.
    在32位寄存器中则是正常形式,如在EAX就是803E7D0C. 当我们的形式地址指向这个数的时候,实际上是指向第一个字节,即0C.
    我们可以指定访问长度是字节, 字或者双字. 假设DS

    EDX]指向第一个字节0C:
    mov AL, byte ptr DS

    EDX] ;把字节0C存入AL
    mov AX, word ptr DS

    EDX] ;把字7D0C存入AX
    mov EAX, dword ptr DS

    EDX] ;把双字803E7D0C存入EAX
    在段的属性中,有一个就是缺省访问宽度.如果缺省访问宽度为双字(在32位系统中经常如此),那么要进行字节或字的访问,就必须用byte/word ptr显式地指明.
    缺省段选择:如果指令中只有作为段内偏移的有效地址,而没有指明在哪一个段里的时候,有如下规则:
    如果用ebp和esp作为基址或间址,则认为是在SS确定的段中;
    其他情况,都认为是在DS确定的段中。
    如果想打破这个规则,就必须使用段超越前缀。举例如下:
    mov eax, dword ptr [edx] ;缺省使用DS,把DS

    EDX]指向的双字送入eax
    mov ebx, dword ptr ES

    EDX] ;使用ES:段超越前缀,把ES

    EDX]指向的双字送入ebx
    堆栈:

    栈是一种数据结构,严格地应该叫做“栈”。“堆”是另一种类似但不同的结构。SS 和 ESP
    是INTEL对栈这种数据结构的硬件支持。push/pop指令是专门针对栈结构的特定操作。SS指定一个段为栈段,ESP则指出当前的栈顶。push
    xxx 指令作如下操作:
    把ESP的值减去4;
    把xxx存入SS

    ESP]指向的内存单元。
    这样,esp的值减小了4,并且SS

    ESP]指向新压入的xxx. 所以栈是“倒着长”的,从高地址向低地址方向扩展。pop yyy 指令做相反的操作,把SS

    ESP]指向的双字送到yyy指定的寄存器或内存单元,然后把esp的值加上4。这时,认为该值已被弹出,不再在栈上了,因为它虽然还暂时存在在原来的栈顶位置,但下一个push操作就会把它覆盖。因此,在栈段中地址低于esp的内存单元中的数据均被认为是未定义的。
    最后,有一个要注意的事实是,汇编语言是面向机器的,指令和机器码基本上是一一对应的,所以它们的实现取决于硬件.有些看似合理的指令实际上是不存在的,比如:
    mov DS

    edx], ds

    ecx] ;内存单元之间不能直接传送
    mov DS, 1A7 ;段寄存器不能直接由立即数赋值
    mov EIP, 3D4E7 ;不能对指令指针直接操作.
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期一, 五月 22, 2006
          
           -->
    arm linux
    滚滚长江东四水,浪花淘净英雄。
    大家好,许多人和我一样,正在苦读linux源代码,希望有照一
    日,宝典在手,天下我有。小弟不才,也读了两年,写的几首歪诗。从本级开始,把我所理解的linux如何启动贴出来,不懂之处大家讨论一番。也希望把
    linux从头到尾讨论一遍,计划写它240回,三年写完(笑。。。),欢迎大家动员一些牛人来参与讨论,提高人气,增加流量。
    小弟用的是arm920T,跑LINUX 2。4。18,下面是第一回。。。。。
    长篇连载--arm linux演艺---第一回
    --------------------------------------------------------------------------------
    话说。。。(嘘声,“入正题把!“)
    好好:
    首先,porting linux的时候要规划内存影像,如小弟的系统有64m SDRAM,
    地址从0x 0800 0000 -0x0bff ffff,32m flash,地址从0x0c00 0000-0x0dff ffff.
    规划如下:bootloader, linux kernel, rootdisk放在flash里。
    具体从 0x0c00 0000开始的第一个1M放bootloader,
    0x0c10 0000开始的2m放linux kernel,从 0x0c30 0000开始都给rootdisk。
    启动:
    首先,启动后arm920T将地址0x0c00 0000映射到0(可通过跳线设置),
    实际上从0x0c00 0000启动,进入我们的bootloader,但由于flash速度慢,
    所以bootloader前面有一小段程序把bootloader拷贝到SDRAM 中的0x0AFE0100,
    再从0x 0800 0000 运行bootloader,我们叫这段小程序为flashloader,
    flashloader必须要首先初始化SDRAM,不然往那放那些东东:
    .equ SOURCE, 0x0C000100 bootloader的存放地址
    .equ TARGET, 0x0AFE0100 目标地址
    .equ SDCTL0, 0x221000 SDRAM控制器寄存器
    // size is stored in location 0x0C0000FC
    .global _start
    _start: //入口点
    //;***************************************
    //;* Init SDRAM
    //;***************************************
    // ;***************
    // ;* SDRAM
    // ;***************
    LDR r1, =SDCTL0 //
    // ; Set Precharge Command
    LDR r3, =0x92120200
    //ldr r3,=0x92120251
    STR r3, [r1]
    // ; Issue Precharge All Commad
    LDR r3, =0x8200000
    LDR r2, [r3]
    // ; Set AutoRefresh Command
    LDR r3, =0xA2120200
    STR r3, [r1]
    // ; Issue AutoRefresh Command
    LDR r3, =0x8000000
    LDR r2, [r3]
    LDR r2, [r3]
    LDR r2, [r3]
    LDR r2, [r3]
    LDR r2, [r3]
    LDR r2, [r3]
    LDR r2, [r3]
    LDR r2, [r3]
    // ; Set Mode Register
    LDR r3, =0xB2120200
    STR r3, [r1]
    // ; Issue Mode Register Command
    LDR r3, =0x08111800 //; Mode Register Value
    LDR r2, [r3]
    // ; Set Normal Mode
    LDR r3, =0x82124200
    STR r3, [r1]
    //;***************************************
    //;* End of SDRAM and SyncFlash Init *
    //;***************************************
    // copy code from FLASH to SRAM
    _CopyCodes:
    ldr r0,=SOURCE
    ldr r1,=TARGET
    sub r3,r0,#4
    ldr r2,[r3]
    _CopyLoop:
    ldr r3,[r0]
    str r3,[r1]
    add r0,r0,#4
    add r1,r1,#4
    sub r2,r2,#4
    teq r2,#0
    beq _EndCopy
    b _CopyLoop
    _EndCopy:
    ldr r0,=TARGET
    mov pc,r0
    欲知后事如何,下回分解:
    长篇连载--arm linux演艺---第二回
    --------------------------------------------------------------------------------
    上回书说到flashloader把bootloader load到0x0AFE0100, 然回跳了过去,
    其实0x0AFE0100 就是烧在flash 0x0C000100中的真正的bootloader:
    bootloader 有几个文件组成,先是START.s,也是唯一的一个汇编程序,其余的都是C写成的,START.s主要初始化堆栈:
    _start:
    ldr r1,=StackInit
    ldr sp,[r1]
    b main
    //此处我们跳到了C代码的main函数,当C代码执行完后,还要调用
    //下面的JumpToKernel0x跳到LINXU kernel运行
    .equ StackInitValue, __end_data+0x1000 // 4K __end_data在连结脚本中指定
    StackInit:
    .long StackInitValue
    .global JumpToKernel
    JumpToKernel:
    // jump to the copy code (get the arguments right)
    mov pc, r0
    .global JumpToKernel0x
    // r0 = jump address
    // r1-r4 = arguments to use (these get shifted)
    JumpToKernel0x:
    // jump to the copy code (get the arguments right)
    mov r8, r0
    mov r0, r1
    mov r1, r2
    mov r2, r3
    mov r3, r4
    mov pc, r8
    .section ".data.boot"
    .section ".bss.boot"
    欲知bootloader中的c代码如何运行,请看下集
    长篇连载--arm linux演艺---第三回
    --------------------------------------------------------------------------------
    书接上回:
    下面让我们看看bootloader的c代码干了些什么。main函数比较长,让我们分段慢慢看。
    int main()
    {
    U32 *pSource, *pDestin, count;
    U8 countDown, bootOption;
    U32 delayCount;
    U32 fileSize, i;
    char c;
    char *pCmdLine;
    char *pMem;
    init(); //初始化FLASH控制器和CPU时钟
    EUARTinit(); //串口初始化
    EUARTputString(" DBMX1 linux Bootloader ver 0.2.0 ");
    EUARTputString("Copyright (C) 2002 Motorola Ltd. ");
    EUARTputString((U8 *)cmdLine);
    EUARTputString(" ");
    EUARTputString("Press any key for alternate boot-up options ... ");
    小弟的bootloader主要干这么几件事:init(); 初始化硬件,打印一些信息和提供一些操作选项:
    0. Program bootloader image
    1. Program kernel image
    2. Program root-disk image
    3. Download kernel and boot from RAM
    4. Download kernel and boot with ver 0.1.x bootloader format
    5. Boot a ver0.1.x kernel
    6. Boot with a different command line
    也就是说,可以在bootloader里选择重新下载kernel,rootdisk并写入flash,
    下载的方法是用usb连接,10m的rootdisk也就刷的一下。关于usb下载的讨论请参看先前的贴子“为arm开发平台增加usb下载接口“。
    如果不选,直接回车,就开始把整个linux的内核拷贝到SDRAM中运行。
    列位看官,可能有人要问,在flashloader中不是已经初始化过sdram控制器了吗?怎么init(); 中还要初始化呢,各位有所不知,小弟用的是syncflash,

    以直接使用sdram控制器的接口,切记:在flash中运行的代码是不能初始化连接flash的sdram控制器的,不然绝对死掉了。所以,当程序在
    flash中运行的时候,去初始化sdram,而现在在sdram中运行,可放心大胆地初始化flash了,主要是设定字宽,行列延时,因为缺省都是最大
    的。
    另外,如果列位看官的cpu有足够的片内ram,完全可以先把bootloader放在片内ram,干完一切后再跳到LINUX,小弟着也是不得已而为之啊。
    今天太晚了,回去睡觉了。。。
    长篇连载--arm linux演艺---第四回
    --------------------------------------------------------------------------------
    如果直接输入回车,进入kernel拷贝工作:
    EUARTputString("Copying kernel from Flash to RAM ... ");
    count = 0x200000; // 2 Mbytes
    pSource = (U32 *)0x0C100000;
    pDestin = (U32 *)0x08008000;
    do
    {
    *(pDestin++) = *(pSource++);
    count -= 4;
    } while (count > 0);
    }
    EUARTputString("Booting kernel ... ");
    这一段没有什么可说的,运行完后kernel就在0x08008000了,至于为什么要
    空出0x8000的一段,主要是放kelnel的一些全局数据结构,如内核页表,arm的页目录要有16k大。
    我们知道,linux内核启动的时候可以传入参数,如在PC上,如果使用LILO,
    当出现LILO:,我们可以输入root=/dev/hda1.或mem=128M等指定文件系统的设备或内存大小,在嵌入式系统上,参数的传入是要靠bootloader完成的,
    pMem = (char *)0x083FF000; //参数字符串的目标存放地址
    pCmdLine = (char *)&cmdLine; //定义的静态字符串
    while ((*(pMem++)=*(pCmdLine++)) != 0);//拷贝
    JumpToKernel((void *)0x8008000, 0x083FF000) ;//跳转到内核
    return (0);
    JumpToKernel在前文中的start.S定义过:
    JumpToKernel:
    // jump to the copy code (get the arguments right)
    mov pc, r0
    .global JumpToKernel0x
    // r0 = jump address
    // r1 = arguments to use (these get shifted)
    由于arm-GCC的c参数调用的顺序是从左到右R0开始,所以R0是KERNKEL的地址,
    r1是参数字符串的地址:
    到此为止,为linux引导做的准备工作就结束了,下一回我们就正式进入linux的代码。
    困了。。。
    长篇连载--arm linux演艺---第五回
    --------------------------------------------------------------------------------
    好,从本节开始,我们走过了bootloader的漫长征途,开始进入linux的内核:
    说实话,linux宝典的确高深莫测,洋人花了十几年修炼,各种内功心法层处不穷。有些地方反复推敲也领悟不了其中奥妙,炼不到第九重啊。。
    linux的入口是一段汇编代码,用于基本的硬件设置和建立临时页表,对于
    ARM LINUX是 linux/arch/arm/kernle/head-armv.S, 走!
    #if defined(CONFIG_MX1)
    mov r1, #MACH_TYPE_MX1
    #endif
    这第一句话好像就让人看不懂,好像葵花宝典开头的八个字:欲练神功。。。。
    那来的MACH_TYPE_MX1?其实,在head-armv.S
    中的一项重要工作就是设置内核的临时页表,不然mmu开起来也玩不转,但是内核怎么知道如何映射内存呢?linux的内核将映射到虚地址0xCxxx xxxx处,但他怎么知道把哪一片ram映射过去呢?
    因为不通的系统有不通的内存影像,所以,LINUX约定,内核代码开始的时候,
    R1放的是系统目标平台的代号,对于一些常见的,标准的平台,内核已经提供了支持,只要在编译的时候选中就行了,例如对X86平台,内核是从物理地址1M开始映射的。如果老兄是自己攒的平台,只好麻烦你自己写了。
    小弟拿人钱财,与人消灾,用的是摩托的MX1,只好自己写了,定义了#MACH_TYPE_MX1,当然,还要写一个描述平台的数据结构:
    MACHINE_START(MX1ADS, "Motorola MX1ADS")
    MAINTAINER("SPS Motorola")
    BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)
    FIXUP(mx1ads_fixup)
    MAPIO(mx1ads_map_io)
    INITIRQ(mx1ads_init_irq)
    MACHINE_END
    看起来怪怪的,但现在大家只要知道他定义了基本的内存映象:RAM从0x08000000开始,i/o空间从0x00200000开始,i/o空间映射到虚拟地址空间
    0xf0200000开始处。摩托的芯片i/o和内存是统一编址的。
    其他的项,在下面的初始化过程中会逐个介绍到。
    好了好了,再看下面的指令:
    mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode //设置为SVC模式,允许中断和快速中断
    //此处设定系统的工作状态,arm有7种状态
    //每种状态有自己的堆栈
    msr cpsr_c, r0 @ and all irqs diabled
    bl __lookup_processor_type
    //定义处理器相关信息,如value, mask, mmuflags,
    //放在proc.info段中
    //__lookup_processor_type 取得这些信息,在下面
    //__lookup_architecture_type 中用
    这一段是查询处理器的种类,大家知道arm有arm7, arm9等类型,如何区分呢?
    在arm协处理器中有一个只读寄存器,存放处理器相关信息。__lookup_processor_type将返回如下的结构:
    __arm920_proc_info:
    .long 0x41009200 //CPU id
    .long 0xff00fff0 //cpu mask
    .long 0x00000c1e @ mmuflags
    b __arm920_setup
    .long cpu_arch_name
    .long cpu_elf_name
    .long HWCAP_SWP | HWCAP_HALF | HWCAP_26BIT
    .long cpu_arm920_info
    .long arm920_processor_functions
    第一项是CPU id,将与协处理器中读出的id作比较,其余的都是与处理器相关的
    信息,到下面初始化的过程中自然会用到。。
    第五回终。。。
    长篇连载--arm linux演艺---第六回
    --------------------------------------------------------------------------------
    查询到了处理器类型和系统的内存映像后就要进入初始化过程中比较关键的一步了,开始设置mmu,但首先要设置一个临时的内核页表,映射4m的内存,这在初始化过程中是足够了:
    //r5=0800 0000 ram起始地址 r6=0020 0000 io地址,r7=f020 0000 虚io
    teq r7, #0 @ invalid architecture?
    moveq r0, #'a' @ yes, error 'a'
    beq __error
    bl __create_page_tables
    其中__create_page_tables为:
    __create_page_tables:
    pgtbl r4
    //r4=0800 4000 临时页表的起始地址
    //r5=0800 0000, ram的起始地址
    //r6=0020 0000, i/o寄存器空间的起始地址
    //r7=0000 3c08
    //r8=0000 0c1e
    //the page table in 0800 4000 is just temp base page, when init_task's sweaper_page_dir ready,
    // the temp page will be useless
    // the high 12 bit of virtual address is base table index, so we need 4kx4 = 16k temp base page,
    mov r0, r4
    mov r3, #0
    add r2, r0, #0x4000 @ 16k of page table
    1: str r3, [r0], #4 @ Clear page table
    str r3, [r0], #4
    str r3, [r0], #4
    str r3, [r0], #4
    teq r0, r2
    bne 1b
    /*
    * Create identity mapping for first MB of kernel.
    * This is marked cacheable and bufferable.
    *
    * The identity mapping will be removed by
    */
    // 由于linux编译的地址是0xC0008000,load的地址是0x08008000,我们需要将虚地址0xC0008000映射到0800800一段
    //同时,由于部分代码也要直接访问0x08008000,所以0x08008000对应的表项也要填充
    // 页表中的表象为section,AP=11表示任何模式下可访问,domain为0。
    add r3, r8, r5 @ mmuflags + start of RAM
    //r3=0800 0c1e
    add r0, r4, r5, lsr #18
    //r0=0800 4200
    str r3, [r0] @ identity mapping
    //*0800 4200 = 0800 0c1e 0x200表象 对应的是0800 0000 的1m
    /*
    * Now setup the pagetables for our kernel direct
    * mapped region. We round TEXTADDR down to the
    * nearest megabyte boundary.
    */
    //下面是映射4M
    add r0, r4, #(TEXTADDR & 0xfff00000) >> 18 @ start of kernel
    //r0 = r4+ 0x3000 = 0800 4000 + 3000 = 0800 7000
    str r3, [r0], #4 @ PAGE_OFFSET + 0MB
    //*0800 7004 = 0800 0c1e
    add r3, r3, #1 wirte函数输出,否则就一直在buffer里呆着。所以,用printk输出的信息,如果超出了
    4k,会冲掉前面的。在系统引导起来后,用dmesg看的也就是这个buffer中的东东。
    待续。。。。。
    长篇连载--arm linux演艺---第九回
    --------------------------------------------------------------------------------
    下面就是一个重量级的函数:
    setup_arch(&command_line); //arm/kernel/setup.c
    完成内存映像的初始化,其中command_line是从bootloader中传下来的。
    void __init setup_arch(char **cmdline_p)
    {
    struct param_struct *params = NULL;
    struct machine_desc *mdesc; //arch structure, for your ads, defined in include/arm-asm/mach/arch.h very long
    struct meminfo meminfo;
    char *from = default_command_line;
    memset(&meminfo, 0, sizeof(meminfo));
    首先把meminfo清零,有个背景介绍一下,从linux 2.4的内核开始,支持内存的节点(node),也就是可支持不连续的物理内存区域。这一点在嵌入式系统中很有用,例如对于SDRAM和FALSH,性质不同,可作为不同的内存节点。
    meminfo结构定义如下:
    /******************************************************/
    #define NR_BANKS 4
    //define the systen mem region, not consistent
    struct meminfo {
    int nr_banks;
    unsigned long end;
    struct {
    unsigned long start;
    unsigned long size;
    int node;
    } bank[NR_BANKS];
    };
    /******************************************************/
    下面是:ROOT_DEV = MKDEV(0, 255);
    ROOT_DEV是宏,指明启动的设备,嵌入式系统中通常是flash disk.
    这里面有一个有趣的悖论:linux的设备都是在/dev/下,访问这些设备文件需要设备驱动程序支持,而访问设备文件才能取得设备号,才能加载驱动程序,那么第一个设备驱动程序是怎么加载呢?就是ROOT_DEV, 不需要访问设备文件,直接指定设备号。
    下面我们准备初始化真正的内核页表,而不再是临时的了。
    首先还是取得当前系统的内存映像:
    mdesc = setup_architecture(machine_arch_type);
    //find the machine type in mach-integrator/arch.c
    //the ads name, mem map, io map
    返回如下结构:
    mach-integrator/arch.c
    MACHINE_START(INTEGRATOR, "Motorola MX1ADS")
    MAINTAINER("ARM Ltd/Deep Blue Solutions Ltd")
    BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)
    FIXUP(integrator_fixup)
    MAPIO(integrator_map_io)
    INITIRQ(integrator_init_irq)
    MACHINE_END
    我们在前面介绍过这个结构,不过这次用它可是玩真的了。
    长篇连载--arm linux演艺---第十回
    --------------------------------------------------------------------------------
    书接上回,
    下面是init_mm的初始化,init_mm定义在/arch/arm/kernel/init_task.c:
    struct mm_struct init_mm = INIT_MM(init_mm);
    从本回开始的相当一部分内容是和内存管理相关的,凭心而论,操作系统的
    内存管理是很复杂的,牵扯到处理器的硬件细节和软件算法,
    限于篇幅所限制,请大家先仔细读一读arm mmu的部分,
    中文参考资料:linux内核源代码情景对话,
    linux2.4.18原代码分析。
    init_mm.start_code = (unsigned long) &_text;
    内核代码段开始
    init_mm.end_code = (unsigned long) &_etext;
    内核代码段结束
    init_mm.end_data = (unsigned long) &_edata;
    内核数据段开始
    init_mm.brk = (unsigned long) &_end;
    内核数据段结束
    每一个任务都有一个mm_struct结构管理任务内存空间,init_mm
    是内核的mm_struct,其中设置成员变量* mmap指向自己,
    意味着内核只有一个内存管理结构,设置* pgd=swapper_pg_dir,
    swapper_pg_dir是内核的页目录,在arm体系结构有16k,
    所以init_mm定义了整个kernel的内存空间,下面我们会碰到内核
    线程,所有的内核线程都使用内核空间,拥有和内核同样的访问
    权限。
    memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
    //clear command array
    saved_command_line[COMMAND_LINE_SIZE-1] = '
    长篇连载--arm linux演艺---第十一回
    --------------------------------------------------------------------------------
      上回我们说到在paging_init中分配了三个页:
    zero_page=0xc0000000
    bad page=0xc0001000
    bad_table=0xc0002000
    但是奇怪的很,在更新的linux代码中只分配了一个
    zero_page,而且在源代码中找不到zero_page
    用在什么地方了,大家讨论讨论吧。
    paging_init的主要工作是在
    void __init memtable_init(struct meminfo *mi)
    中完成的,为系统内存创建页表:
    meminfo结构如下:
    struct meminfo {
    int nr_banks;
    unsigned long end;
    struct {
    unsigned long start;
    unsigned long size;
    int node;
    } bank[NR_BANKS];
    };
    是用来纪录系统中的内存区段的,因为在嵌入式
    系统中并不是所有的内存都能映射,例如sdram只有
    64m,flash 32m,而且不见得是连续的,所以用
    meminfo纪录这些区段。
    void __init memtable_init(struct meminfo *mi)
    {
    struct map_desc *init_maps, *p, *q;
    unsigned long address = 0;
    int i;
    init_maps = p = alloc_bootmem_low_pages(PAGE_SIZE);
    其中map_desc定义为:
    struct map_desc {
    unsigned long virtual;
    unsigned long physical;
    unsigned long length;
    int domain:4, //页表的domain
    prot_read:1, //保护标志
    prot_write:1, //写保护标志
    cacheable:1, //是否cache
    bufferable:1, //是否用write buffer
    last:1; //空
    };init_maps
    map_desc是区段及其属性的定义,属性位的意义请
    参考ARM MMU的介绍。
    下面对meminfo的区段进行遍历,同时填写init_maps
    中的各项内容:
    for (i = 0; i nr_banks; i++) {
    if (mi->bank.size == 0)
    continue;
    p->physical = mi->bank.start;
    p->virtual = __phys_to_virt(p->physical);
    p->length = mi->bank.size;
    p->domain = DOMAIN_KERNEL;
    p->prot_read = 0;
    p->prot_write = 1;
    p->cacheable = 1; //可以CACHE
    p->bufferable = 1; //使用write buffer
    p ++; //下一个区段
    }
    如果系统有flash,
    #ifdef FLUSH_BASE
    p->physical = FLUSH_BASE_PHYS;
    p->virtual = FLUSH_BASE;
    p->length = PGDIR_SIZE;
    p->domain = DOMAIN_KERNEL;
    p->prot_read = 1;
    p->prot_write = 0;
    p->cacheable = 1;
    p->bufferable = 1;
    p ++;
    #endif
    其中的prot_read和prot_write是用来设置页表的domain的,
    下面就是逐个区段建立页表:
    q = init_maps;
    do {
    if (address virtual || q == p) {
    clear_mapping(address);
    address += PGDIR_SIZE;
    } else {
    create_mapping(q);
    address = q->virtual + q->length;
    address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
    q ++;
    }
    } while (address != 0);

    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    ROM监控器(ROM monitor)
    ROM监控器是一小程序,驻留在嵌入系统ROM中,通过串行的或网络的连接和运行在工作站上的调试软件通信。这是一种便宜的方式,当然也是最低端的技术。

    除了要求一个通信端口和少量的内存空间外,不需要其它任何专门的硬件。并提供了如下功能:下载代码、运行控制、断点、单步步进、以及观察、修改寄存器和内
    存。
    因为ROM监控器是操作软件的一部分,只有当你的应用程序运行时,它才会工作。如果你想检查CPU和应用程序的状态,你就必须停下应用程序,再次进入
    ROM监控器。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    嵌入式实时操作系统ECOS在S3C2510上的移植实现
    作者:华南理工大学电子与信息学院 赵楚莹 尹俊勋
    转贴自:
    http://www.eaw.com.cn


    要:本文介绍了实时操作系统ECOS的特点及基本结构,并具体研究了ECOS在三星公司以ARM940T为内核的S3C2510嵌入式芯片上的移植方法。
    文章着重讨论了移植过程中的重点与难点部分:ECOS的硬件抽象层(HAL)移植。该移植方案已经过实际测试,系统稳定可靠,可运行多任务式应用程序。
    关键词:实时操作系统;ECOS;硬件抽象层;移植;ARM
    引言
    ECOS(Embedded
    Configurable Operating
    System,嵌入式可配置操作系统)是一种针对16位、32位和64位处理器的可移植嵌入式实时操作系统。由于其源代码是公开的,因而有越来越多的设计
    人员开始关注ECOS操作系统。ECOS最大的特点是模块化,内核可配置。最小版本的ECOS只有几百字节,非常适合小型嵌入式系统的开发。相对于嵌入式
    Linux来说,ECOS有配置灵活和节省资源的优势。它的另一个优点是使用多任务抢占机制,具有最小的中断延迟,支持嵌入式系统所需的所有同步原语,并
    拥有灵活的调度策略和中断处理机制,因而具有良好的实时性。与Clinux和COS等操作系统相比,ECOS更适合于处理实时信号的设备,如移动通
    信、WLAN等通信设备的开发。
    S3C2510是一款低功耗、高效能、面向以太网系统的微处理器。它的系统时钟可达133MHz,并包含了
    16/32位宽的ARM940T核、4KB的I-CACHE和4KB的D-CACHE。S3C2510带有两个独立的10/100Mbps的以太网控制
    器,这两个接口能够以硬件完成IEEE802.3的MAC层处理,因此更适合用作SOHO路由器、internet网关,甚至宽带无线接入设备的开发。
    ECOS操作系统也非常适合这些网络设备的开发,本文将介绍S3C2510的移植方案,给各种以ARM为内核处理器的ECOS底层移植开发提供一个系统的
    范例。

    500){this.resized=true;this.style.width=500;}" align="left" border="0" />







    图 1    ECOS操作系统结构图
    ECOS底层移植的基础知识
    ECOS
    系统的主要组成部分如图1所示。操作系统的主要功能
    及特点是由其内核所决定的,底层移植一般不会涉及到系统内核的内容。由图1可见,硬件抽象层是嵌入式操作系统和硬件直接接触的基本层,其将系统内核和具体
    的硬件平台彻底隔离开,
    实现了系统内核与硬件的无关性,这就是操作系统具有良好可移植性的体现。因此,对于开发人员来说,移植操作系统真正的意义和工作在于移植操作系统的硬件抽
    象层。
    硬件抽象层HAL对处理器结构和系统硬件平台进行抽象,当要在一个新的目标平台上运行ECOS时,只需要对底层的硬件抽象层进行修改,便可
    迅速地将整个ECOS系统移植到新的平台上。硬件抽象层主要包括三大模块——体系结构抽象层(Architecture
    HAL)、变体抽象层(Variant HAL)和平台抽象层(Platform
    HAL)。体系结构抽象层主要是指ECOS所支持的具有不同体系结构的处理器系列,如ARM系列、PowerPC系列、MIPS系列等等。变体抽象层指的
    是处理器系列中某款处理器在Cache、MMU和FPU等方面所具有的特殊性。如S3C2510属于ARM系列中的ARM940T,在变体抽象层中就会具
    体地针对ARM940T的Cache等方面作出定义。平台抽象层则是对当前系统硬件平台的抽象,包括了平台的启动、芯片选择与配置、定时设备、I/O寄存
    器访问以及中断寄存器等等。平台抽象层代码的编写是ECOS移植工作的重点。
    HAL移植的主要步骤
    建立适当的文件目录
    ECOS
    本身有一个完整的文件目录,只有把新建的底层文件放在适当的文件目录下面,才能确保配置和编译工作的成功,也有助于利用ECOS本身已有的源代码,如结构
    体系层和变体层中的许多成熟可用的代码。由于本系统中S3C2510处理器的内核是ARM940T,因而可以把S3C2510的目录建立在ECOS库路径
    packages/hal/arm/arm9/下。
    建立S3C2510的cdl文件
    cdl文件使用cdl脚本语言描述该硬件设备(包或平台)的特性和常用指标。cdl文件实现系统在源码级的功能和指标配置,犹如一个项目管理高层对其仓库中组件特性的登记,只有登记后的包、组件和选项才能被操作系统配置工具识别和配置。
    以下是S3C2510的cdl文件中的几段重要描述。
    * cdl_package CYGPKG_ HAL_ARM_ ARM9_S3C2510
    这是S3C2510在ecos.db中所登记的包的名字,它下面包含了该板的一些基本设置和组件,如母体体系结构(parent)、包含的头文件、编译的C文件等。
    * cdl_component CYG_HAL _STARTUP
    系统启动方式,有3种选择:ram启动、rom启动、romram启动。
    * cdl_component CYGNUM_ HAL_CPUCLOCK
    平台的系统时钟设置,以便于ECOS其他组件以此时钟为标准。该平台系统时钟的默认值设为133MHz。
    * cdl_option CYGNUM_HAL_ RTC_PERIOD
    ECOS内核的运行时钟单位。ECOS内核以一个tick为时钟单位,而一个tick的长度就等于该选项的设定值。
    在ecos.db中登记
    S3C2510的硬件包
    ecos.db
    是关于ECOS系统的一个数据库文件(在packages目录下),它包含了硬件包管理工具和一些在组件配置库中的包。与cdl文件相比,ecos.db
    登记了仓库中的物品,而cdl文件则登记每种物品的特性。只有在ecos.db中登记了的包,才能被ECOS的库编译工具(configtool)选中和
    使用。如果要在配置工具的模板选项中(template)增加可供选择的硬件目标板,那么,需要先在ecos.db中登记其包描述,再增加其目标板描述。
    一般的辅助硬件(如网卡、串口等)只需要第一步的登记。因此,在ecos.db中登记S3C2510平台硬件包的基本步骤就是登记硬件平台的包描述
    (package CYGPKG_HAL_ARM_ ARM9_ S3C2510)和目标描述(target
    S3C2510)。需要注意的是,target
    S3C2510中所包含的3个硬件描述包CYGPKG_HAL_ARM、CYGPKG_HAL_ARM_ARM9和
    CYGPKG_HAL_ARM_ARM9_
    S3C2510是不能缺少的,因为它们是标板的核心——主体系结构包、子体系结构包和主芯片包。另外,还可以可选地添加其他辅助硬件包(如网卡、串口
    等)。
    编写平台抽象层的有关代码
    硬件平台层所需编写的代码文件的一般功能如下所示。
    * include /plf_cache.h —— 平台专用cache处理 (可选)。在本系统中不需要编写,可直接调用ARM9变体层的hal_cache.h。
    * include / hal_platform_ints.h —— 平台专用中断处理,定义平台中断向量号。
    * include / plf_io.h —— I/O 定义和系统寄存器的宏定义。
    * include ¬/ hal_platform_setup.h —— 平台启动代码。本文件主要用ARM汇编指令编写,实现平台上电后程序的启动和执行。
    * src/s3c2510_misc.c —— HAL的底层标准函数,包括时钟平台初始化、时钟延时函数、中断使能、中断屏蔽、中断响应等。
    * src/ hal_diag.c —— 硬件抽象层诊断输出函数,包含ECOS系统中printf打印的硬件设备驱动程序。
    * misc/ redboot_primary_ ram.ecm —— 基于RAM启动方式的redboot最小配置文件。
    * misc/redboot_primary_ rom.ecm —— 基于ROM启动方式的redboot最小配置文件。
    硬件启动过程

    写硬件启动的初始化过程是HAL移植的一个难点。当硬件重新上电后,系统的程序指针会自动指向地址0(通常地址0存放着bootloader代码段)。在
    ECOS操作系统中,程序首先会运行vectors.S文件(该文件存在于hal/arm/arch/src/目录下),它定义了
    reset_vector、start等各种启动标号。接着调用S3C2510平台层的hal_platform_
    setup.h文件中的宏platform_setup1和arm9变体层arm9_misc.c文件中的函数 hal_hardware_init。
    hal_platform_setup.h
    定义了宏platform_setup1以供vectors.S调用。该宏定义了目标板上SDRAM和FLASH的初始化启动,其中包括了它们的取数方式
    和内存大小。然后根据不同的启动方式执行程序。对于RAM启动方式,无需进行程序段与数据段的搬移,系统已认为SDRAM的起始地址即为程序的起始地址;
    对于ROM启动方式,需要搬移数据段,而程序段无需搬移;对于ROMRAM启动方式,程序段与数据段都需要进行搬移,然后再把程序起始地址映射为
    SDRAM的起始地址。
    在程序搬移完成后,系统会进行其他硬件的初始化过程,包括系统时钟、系统CACHE、监控串口等基本硬件设备。
    内存布局文件编写

    台的内存布局文件在include/pkgconf目录下。通常,每个平台包括了RAM、ROM和ROMRAM
    3种不同启动方式的内存布局文件集。每种启动方式的内存布局文件集都由3个类型的描述文件组成:.h文件包含内存域的C宏定义;.ldi文件定义内存域和
    内存段位置的链接脚本文件;.mlt文件包括由MLT工具产生的对内存布局的描述。当需要手动修改内存布局时,只有.h和.ldi文件可以被修改,.
    mlt文件只能由MLT工具生成。
    下面以S3C2510的ram启动方式内存布局为例,主要说明mlt_arm_s3c2510_ram.h和mlt_arm_s3c2510_ram.ldi的程序结构。
    由于S3C2510的开发板有两个16MB的SDRAM,因而要定义两个内存域ram1和ram2。系统设置寄存器在初始化时已经把内存段重新映射,因而两个SDRAM的基地址就是0x0和0x40000000,两个内存域的大小是16MB,分配方式都是可读写的内存段。
    在mlt_arm_s3c2510_ram.ldi
    中分为两大部分。首先是MEMORY部分,它定义了在RAM启动方式下所需要的内存域,以及该内存域的起始地址和长度。MEMORY部分的内容必须与
    mlt_arm_s3c2510_
    ram.h中定义的宏一致。其次是SECTIONS部分,它定义了RAM启动方式下所规定的内存段,这些内存段的定义与系统内存管理功能有关。在
    SECTION_XXX后带有相应的参数,这些参数包括了内存段所属的内存域、起始地址(或者是对齐方式)、虚拟内存地址(VMA)和加载内存地址
    (LMA)。
    以SECTION_fixed_vectors (ram1, 0x200,
    LMA_EQ_VMA)为例,它表示fixed_vectors段属于ram1内存域,起始地址为0x200,加载内存地址等于虚拟内存地址。
    LMA_EQ_VMA同时也可以解释为该内存段不需要在程序运行后重新分配加载。
    调试结果
    S3C2510目标板上带有1块4MB的FLASH和2块16MB的SDRAM。

    用ECOS的自带编译工具
    configtool对新建的S3C2510目标板进行编译,生成ECOS的库文件。然后把库目录下的install目录内容复制到应用工程目录下,使
    ECOS库包含到应用工程中。然后把该工程的.elf文件利用EMBEST公司开发的IDE仿真器直接下载到目标板的SDRAM中。此时的ECOS操作系
    统应为RAM启动方式。
    通过IDE对程序的调试与测试结果表明,本文提出的S3C2510移植方案使ECOS操作系统在目标板中运行正常稳定。该操作系统支持多个工作线程的应用程序。S3C2510的串口、网口均能与pc机正常传输数据。
    结语
    ECOS
    是一款非常年轻的嵌入式操作系统,1997年才正式推广使用。现阶段有关ECOS开发的参考资料和专门从事人员仍然很少,造成了ECOS产品研发周期和开
    发成本的增加。因此,本文提出的ECOS操作系统的驱动底层代码编写方法对于使用ECOS开发产品具有相当重要的指导意义。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期五, 五月 19, 2006
          
           -->
    objcopy
    GNU实用工具程序objcopy的作用是拷贝一个目标文件的内容到另一个目标文件中。Objcopy使用GNU
    BFD库去读或写目标文件。Objcopy可以使用不同于源目标文件的格式来写目的目标文件(也即是说可以将一种格式的目标文件转换成另一种格式的目标文
    件)。通过以上命令行选项可以控制Objcopy的具体操作。
    Objcopy在进行目标文件的转换时,将生成一个临时文件,转换完成后就将这个临
    时文件删掉。Objcopy使用BFD做转换工作。如果没有明确地格式要求,则Objcopy将访问所有在BFD库中已经描述了的并且它可以识别的格式,
    请参见《GNUpro Decelopment Tools》中“using ld”一章中“BFD库”部分和“BFD库中规范的目标文件格式”部分。
    通过使用srec作为输出目标(使用命令行选项-o srec),Objcopy可以产生S记录格式文件。

    过使用binary作为输出目标(使用命令行选项-o
    binary),Objcopy可以产生原始的二进制文件。使用Objcopy产生一个原始的二进制文件,实质上是进行了一回输入目标文件内容的内存转
    储。所有的符号和重定位信息都将被丢弃。内存转储起始于输入目标文件中那些将要拷贝到输出目标文件去的部分的最小虚地址处。
    使用Objcopy生成S记录格式文件或者原始的二进制文件的过程中,-S选项和-R选项可能会比较有用。-S选项是用来删掉包含调试信息的部分,-R选项是用来删掉包含了二进制文件不需要的内容的那些部分。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    GNU make 指南
      GNU make 指南
    翻译: 哈少
    译者按: 本文是一篇介绍 GNU
    Make 的文章,读完后读者应该基本掌握了 make 的用法。而 make 是所有想在 Unix (当然也包括 Linux
    )系统上编程的用户必须掌握的工具。如果你写的程序中没有用到 make
    ,则说明你写的程序只是个人的练习程序,不具有任何实用的价值。也许这么说有点儿偏激,但 make
    实在是应该用在任何稍具规模的程序中的。希望本文可以为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux
    用户除了学会安装红帽子以外, 实在应该尝试写一些有用的程序。个人想法,大家参考。
    C-Scene 题目 #2
    多文件项目和 GNU Make 工具
    作者: 乔治富特 (Goerge Foot)
    电子邮件: george.foot@merton.ox.ac.uk
    Occupation: Student at Merton College, Oxford University, England
    职业:学生,默尔顿学院,牛津城大学,英格兰
    IRC匿名: gfoot
    拒绝承诺:作者对于任何因此而对任何事物造成的所有损害(你所拥有或不 拥有的实际的,抽象的,或者虚拟的)。所有的损坏都是你自己的责任,而 与我无关。

    有权: “多文件项目”部分属于作者的财产,版权归乔治富特1997年 五月至七月。其它部分属 CScene 财产,版权 CScene
    1997年,保留所有 版权。本 CScene 文章的分发,部分或全部,应依照所有其它 CScene 的文章 的条件来处理。
    0) 介绍
    ~~~~~~~~~~~~~~~

    文将首先介绍为什么要将你的C源代码分离成几个合理的独立档案,什么时 候需要分,怎么才能分的好。然后将会告诉你 GNU Make
    怎样使你的编译和连 接步骤自动化。对于其它 Make
    工具的用户来说,虽然在用其它类似工具时要做适当的调整,本文的内容仍然是非常有用的。如果对你自己的编程工具有怀
    疑,可以实际的试一试,但请先阅读用户手册。
    1) 多文件项目
    ~~~~~~~~~~~~~~~~~~~~~~
    1.1为什么使用它们?
    首先,多文件项目的好处在那里呢?
    它们看起来把事情弄的复杂无比。又要 header 文件,又要 extern 声明,而且如果需要查找一个文件,你要在更多的文件里搜索。

    其实我们有很有力的理由支持我们把一个项目分解成小块。当你改
    动一行代码,编译器需要全部重新编译来生成一个新的可执行文件。但如果你的项目是分开在几个小文件里,当你改动其中一个文件的时
    候,别的源文件的目标文件(object files)已经存在,所以没有什么原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文
    件,然后重新连接所有的目标文件罢了。在大型的项目中,这意味着从很长的(几分钟到几小时)重新编译缩短为十几,二十几秒的简单 调整。
    只要通过基本的规划,将一个项目分解成多个小文件可使你更加容易 的找到一段代码。很简单,你根据代码的作用把你的代码分解到不同 的文件里。当你要看一段代码时,你可以准确的知道在那个文件中去 寻找它。

    很多目标文件生成一个程序包 (Library)比从一个单一的大目标文件
    生成要好的多。当然实际上这是否真是一个优势则是由你所用的系统来决定的。但是当使用 gcc/ld (一个 GNU C 编译/连接器) 把一个程
    序包连接到一个程序时,在连接的过程中,它会尝试不去连接没有使用到的部分。但它每次只能从程序包中把一个完整的目标文件排除在
    外。因此如果你参考一个程序包中某一个目标档中任何一个符号的话,那么这个目标文件整个都会被连接进来。要是一个程序包被非常充分
    的分解了的话,那么经连接后,得到的可执行文件会比从一个大目标文件组成的程序包连接得到的文件小得多。
    又因为你的程序是很模块化的,文件之间的共享部分被减到最少,那 就有很多好处——可以很容易的追踪到臭虫,这些模块经常是可以用 在其它的项目里的,同时别人也可以更容易的理解你的一段代码是干 什么的。当然此外还有许多别的好处……
    1.2 何时分解你的项目

    明显,把任何东西都分解是不合理的。象“世界,你们好”这样的
    简单程序根本就不能分,因为实在也没什么可分的。把用于测试用的小程序分解也是没什么意思的。但一般来说,当分解项目有助于布局、
    发展和易读性的时候,我都会采取它。在大多数的情况下,这都是适用的。(所谓“世界,你们好”,既 'hello world' ,只是一个介
    绍一种编程语言时惯用的范例程序,它会在屏幕上显示一行 'hello world' 。是最简单的程序。)
    如果你需要开发一个相当
    大的项目,在开始前,应该考虑一下你将
    如何实现它,并且生成几个文件(用适当的名字)来放你的代码。当然,在你的项目开发的过程中,你可以建立新的文件,但如果你
    这么做的话,说明你可能改变了当初的想法,你应该想想是否需要对整体结构也进行相应的调整。
    对于中型的项目,你当然也可以采用上述技巧,但你
    也可以就那么开 始输入你的代码,当你的码多到难以管理的时候再把它们分解成不同的档案。但以我的经验来说,开始时在脑子里形成一个大概的方案,
    并且尽量遵从它,或在开发过程中,随着程序的需要而修改,会使开 发变得更加容易。
    1.3 怎样分解项目
    先说明,这完全是我个人的意见,你可以(也许你真的会?)用别的 方式来做。这会触动到有关编码风格的问题,而大家从来就没有停止 过在这个问题上的争论。在这里我只是给出我自己喜欢的做法(同时 也给出这么做的原因):
    i) 不要用一个 header 文件指向多个源码文件(例外:程序包 的 header 文件)。用一个 header定义一个源码文件的方式 会更有效,也更容易查寻。否则改变一个源文件的结构(并且 它的 header 文件)就必须重新编译好几个文件。
    ii)
    如果可以的话,完全可以用超过一个的 header 文件来指向同
    一个源码文件。有时将不可公开调用的函数原型,类型定义等等,从它们的C源码文件中分离出来是非常有用的。使用一 个 header
    文件装公开符号,用另一个装私人符号意味着如果你改变了这个源码文件的内部结构,你可以只是重新编译它而 不需要重新编译那些使用它的公开
    header 文件的其它的源文 件。
    iii) 不要在多个 header 文件中重复定义信息。 如果需要, 在其中一个
    header 文件里 #include 另一个,但是不要重复输入相同的 header 信息两次。原因是如果你以后改
    变了这个信息,你只需要把它改变一次,不用搜索并改变另外一 个重复的信息。
    iv) 在每一个源码文件里, #include 那些声明了源码文件中的符 号的所有 header 文件。这样一来,你在源码文件和 header 文件对某些函数做出的矛盾声明可以比较容易的被编译器发现。
    1.4 对于常见错误的注释
    a)
    定义符 (Identifier) 在源码文件中的矛盾:在C里,变量和函数的缺
    省状态是公用的。因此,任何C源码档案都可以引用存在于其它源码档中的通用 (global) 函数和通用变量,既使这个档案没有那个变
    量或函数的声明或原型。因此你必须保证在不同的两个档案里不能用同一个符号名称,否则会有连接错误或者在编译时会有警告。
    一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有 关的前缀。比如:所有在 gfx.c 里的函数都加上前缀“gfx_”。如果 你很小心的分解你的程序,使用有意义的函数名称,并且不是过分 使用通用变量,当然这根本就不是问题。
    要防止一个符号在它被定义的源文件以外被看到,可在它的定义前 加上关键字“static”。这对只在一个档案内部使用,其它档案都 都不会用到的简单函数是很有用的。
    b)
    多次定义的符号: header 档会被逐字的替换到你源文件里 #include 的位置的。因此,如果 header 档被 #include
    到一个以上的源文件 里,这个 header
    档中所有的定义就会出现在每一个有关的源码文件里。这会使它们里的符号被定义一次以上,从而出现连接错误(见 上)。
    解决方法: 不要在
    header 档里定义变量。你只需要在 header 档里声明它们然后在适当的C源码文件(应该 #include 那个 header
    档的那个)里定义它们(一次)。对于初学者来说,定义和声明是
    很容易混淆的。声明的作用是告诉编译器其所声明的符号应该存在,并且要有所指定的类型。但是,它并不会使编译器分配贮存空间。
    而定义的做用是要求编译器分配贮存空间。当做一个声明而不是做定义的时候,在声明前放一个关键字“extern”。
    例如,我们有一个叫“counter”的变量,如果想让它成为公用的, 我们在一个源码程序(只在一个里面)的开始定义它:“int counter;”,再在相关的 header 档里声明它:“extern int counter;”。
    函数原型里隐含着 extern 的意思,所以不需顾虑这个问题。
    c) 重复定义,重复声明,矛盾类型:

    考虑如果在一个C源码文件中 #include 两个档 a.h 和 b.h, 而 a.h 又 #include 了 b.h 档(原因是 b.h
    档定义了一些 a.h 需要的类型),会发生什么事呢?这时该C源码文件 #include 了 b.h 两次。因此每一个在 b.h 中的
    #define 都发生了两次,每一
    个声明发生了两次,等等。理论上,因为它们是完全一样的拷贝,所以应该不会有什么问题,但在实际应用上,这是不符合C的语法
    的,可能在编译时出现错误,或至少是警告。
    解决的方法是要确定每一个 header 档在任一个源码文件中只被包
    含了一次。我们一般是用预处理器来达到这个目的的。当我们进入 每一个 header 档时,我们为这个 header 档 #define 一个巨集
    指令。只有在这个巨集指令没有被定义的前提下,我们才真正使用 该 header 档的主体。在实际应用上,我们只要简单的把下面一段 码放在每一个
    header 档的开始部分:
    #ifndef FILENAME_H
    #define FILENAME_H
    然后把下面一行码放在最后:
    #endif
    用 header 档的档名(大写的)代替上面的 FILENAME_H,用底线 代替档名中的点。有些人喜欢在 #endif 加上注释来提醒他们这个 #endif 指的是什么。例如:
    #endif /* #ifndef FILENAME_H */
    我个人没有这个习惯,因为这其实是很明显的。当然这只是各人的 风格不同,无伤大雅。
    你只需要在那些有编译错误的 header 档中加入这个技巧,但在所 有的 header 档中都加入也没什么损失,到底这是个好习惯。
    1.5 重新编译一个多文件项目

    楚的区别编译和连接是很重要的。编译器使用源码文件来产生某种 形式的目标文件(object
    files)。在这个过程中,外部的符号参考并没有被解释或替换。然后我们使用连接器来连接这些目标文件和一些
    标准的程序包再加你指定的程序包,最后连接生成一个可执行程序。在这个阶段,一个目标文件中对别的文件中的符号的参考被解释,并
    报告不能被解释的参考,一般是以错误信息的形式报告出来。
    基本的步骤就应该是,把你的源码文件一个一个的编译成目标文件的格
    式,最后把所有的目标文件加上需要的程序包连接成一个可执行文件。具体怎么做是由你的编译器决定的。这里我只给出 gcc (GNU C 编译
    器)的有关命令,这些有可能对你的非 gcc 编译器也适用。
    gcc 是一个多目标的工具。它在需要的时候呼叫其它的元件(预处理 程序,编译器,组合程序,连接器)。具体的哪些元件被呼叫取决于 输入文件的类型和你传递给它的开关。
    一般来说,如果你只给它C源码文件,它将预处理,编译,组合所有 的文件,然后把所得的目标文件连接成一个可执行文件(一般生成的 文件被命名为 a.out )。你当然可以这么做,但这会破坏很多我们 把一个项目分解成多个文件所得到的好处。

    果你给它一个 -c 开关,gcc 只把给它的文件编译成目标文件,
    用源码文件的文件名命名但把其后缀由“.c”或“.cc”变成“.o”。如果你给它的是一列目标文件, gcc 会把它们连接成可执行文件,
    缺省文件名是 a.out 。你可以改变缺省名,用开关 -o 后跟你指定的文件名。
    因此,当你改变了一个源码文件后,你需要重新编译它:
    'gcc -c filename.c' 然后重新连接你的项目: 'gcc -o exec_filename *.o'。 如果你改变了一个
    header 档,你需要重新编译所有 #include 过这个档的源码文件,你可以用 'gcc -c file1.c file2.c
    file3.c' 然后象上边一样连接。
    当然这么做是很繁琐的,幸亏我们有些工具使这个步骤变得简单。 本文的第二部分就是介绍其中的一件工具:GNU Make 工具。
    (好家伙,现在才开始见真章。您学到点儿东西没?)
    2) GNU Make 工具
    ~~~~~~~~~~~~~~~~
    2.1 基本 makefile 结构
    GNU
    Make 的主要工作是读进一个文本文件, makefile 。这个文
    件里主要是有关哪些文件(‘target’目的文件)是从哪些别的文件(‘dependencies’依靠文件)中产生的,用什么命令来进行
    这个产生过程。有了这些信息, make 会检查磁碟上的文件,如果目的文件的时间戳(该文件生成或被改动时的时间)比至少它的一 个依靠文件旧的话,
    make 就执行相应的命令,以便更新目的文件。(目的文件不一定是最后的可执行档,它可以是任何一个文件。)
    makefile 一般被叫做“makefile”或“Makefile”。当然你可以 在 make 的命令行指定别的文件名。如果你不特别指定,它会寻 找“makefile”或“Makefile”,因此使用这两个名字是最简单 的。
    一个 makefile 主要含有一系列的规则,如下:
    : ...
    (tab)
    (tab)
    .
    .
    .
    例如,考虑以下的 makefile :
    === makefile 开始 ===
    myprog : foo.o bar.o
    gcc foo.o bar.o -o myprog
    foo.o : foo.c foo.h bar.h
    gcc -c foo.c -o foo.o
    bar.o : bar.c bar.h
    gcc -c bar.c -o bar.o
    === makefile 结束 ===

    是一个非常基本的 makefile —— make 从最上面开始,把上
    面第一个目的,‘myprog’,做为它的主要目标(一个它需要保证其总是最新的最终目标)。给出的规则说明只要文件‘myprog’
    比文件‘foo.o’或‘bar.o’中的任何一个旧,下一行的命令将会被执行。
    但是,在检查文件 foo.o 和 bar.o
    的时间戳之前,它会往下查 找那些把 foo.o 或 bar.o 做为目标文件的规则。它找到的关于 foo.o 的规则,该文件的依靠文件是
    foo.c, foo.h 和 bar.h 。
    它从下面再找不到生成这些依靠文件的规则,它就开始检查磁碟上这些依靠文件的时间戳。如果这些文件中任何一个的时间戳比 foo.o 的新,命令
    'gcc -o foo.o foo.c' 将会执行,从而更新文件 foo.o 。
    接下来对文件 bar.o 做类似的检查,依靠文件在这里是文件 bar.c 和 bar.h 。
    现在, make 回到‘myprog’的规则。如果刚才两个规则中的任 何一个被执行,myprog 就需要重建(因为其中一个 .o 档就会比 ‘myprog’新),因此连接命令将被执行。

    望到此,你可以看出使用 make 工具来建立程序的好处——前 一章中所有繁琐的检查步骤都由 make
    替你做了:检查时间戳。你的源码文件里一个简单改变都会造成那个文件被重新编译(因 为 .o 文件依靠 .c
    文件),进而可执行文件被重新连接(因为 .o 文件被改变了)。其实真正的得益是在当你改变一个 header
    档的时候——你不再需要记住那个源码文件依靠它,因为所有的 资料都在 makefile 里。 make 会很轻松的替你重新编译所有那
    些因依靠这个 header 文件而改变了的源码文件,如有需要,再进行重新连接。
    当然,你要确定你在 makefile 中所写的规则是正确无误的,只 列出那些在源码文件中被 #include 的 header 档……
    2.2 编写 make 规则 (Rules)

    明显的(也是最简单的)编写规则的方法是一个一个的查 看源码文件,把它们的目标文件做为目的,而C源码文件和被它 #include 的
    header 档做为依靠文件。但是你也要把其它被这些 header 档 #include 的 header
    档也列为依靠文件,还有那些被包括的文件所包括的文件……然后你会发现要对越来越多的文件
    进行管理,然后你的头发开始脱落,你的脾气开始变坏,你的脸色变成菜色,你走在路上开始跟电线杆子碰撞,终于你捣毁你的
    电脑显示器,停止编程。到低有没有些容易点儿的方法呢?
    当然有!向编译器要!在编译每一个源码文件的时候,它实在应
    该知道应该包括什么样的 header 档。使用 gcc 的时候,用 -M 开关,它会为每一个你给它的C文件输出一个规则,把目标文件
    做为目的,而这个C文件和所有应该被 #include 的 header 文件将做为依靠文件。注意这个规则会加入所有 header 文件,包
    括被角括号(`')和双引号(`"')所包围的文件。其实我们可以 相当肯定系统 header 档(比如 stdio.h, stdlib.h
    等等)不会 被我们更改,如果你用 -MM 来代替 -M 传递给 gcc,那些用角括号包围的 header
    档将不会被包括。(这会节省一些编译时间)
    由 gcc 输出的规则不会含有命令部分;你可以自己写入你的命令 或者什么也不写,而让 make 使用它的隐含的规则(参考下面的 2.4 节)。
    2.3 Makefile 变量
    上面提到 makefiles 里主要包含一些规则。它们包含的其它的东 西是变量定义。
    makefile
    里的变量就像一个环境变量(environment variable)。 事实上,环境变量在 make 过程中被解释成 make 的变量。这些
    变量是大小写敏感的,一般使用大写字母。它们可以从几乎任何 地方被引用,也可以被用来做很多事情,比如:
    i) 贮存一个文件名列表。在上面的例子里,生成可执行文件的 规则包含一些目标文件名做为依靠。在这个规则的命令行 里同样的那些文件被输送给 gcc 做为命令参数。如果在这 里使用一个变数来贮存所有的目标文件名,加入新的目标 文件会变的简单而且较不易出错。
    ii) 贮存可执行文件名。如果你的项目被用在一个非 gcc 的系 统里,或者如果你想使用一个不同的编译器,你必须将所有使用编译器的地方改成用新的编译器名。但是如果使用一 个变量来代替编译器名,那么你只需要改变一个地方,其 它所有地方的命令名就都改变了。
    iii) 贮存编译器旗标。假设你想给你所有的编译命令传递一组 相同的选项(例如 -Wall -O -g);如果你把这组选项存入一个变量,那么你可以把这个变量放在所有呼叫编译器 的地方。而当你要改变选项的时候,你只需在一个地方改 变这个变量的内容。
    要设定一个变量,你只要在一行的开始写下这个变量的名字,后 面跟一个 = 号,后面跟你要设定的这个变量的值。以后你要引用 这个变量,写一个 $ 符号,后面是围在括号里的变量名。比如在 下面,我们把前面的 makefile 利用变量重写一遍:
    === makefile 开始 ===
    OBJS = foo.o bar.o
    CC = gcc
    CFLAGS = -Wall -O -g
    myprog : $(OBJS)
    $(CC) $(OBJS) -o myprog
    foo.o : foo.c foo.h bar.h
    $(CC) $(CFLAGS) -c foo.c -o foo.o
    bar.o : bar.c bar.h
    $(CC) $(CFLAGS) -c bar.c -o bar.o
    === makefile 结束 ===
    还有一些设定好的内部变量,它们根据每一个规则内容定义。三个 比较有用的变量是 $@, $ depends

    这里如果一个叫 'depends' 的文件不存在,或任何一个源码文件 比一个已存在的 depends 文件新,那么一个 depends
    文件会被生 成。depends 文件将会含有由 gcc 产生的关于源码文件的规则(注 意 -M 开关)。现在我们要让 make 把这些规则当做
    makefile 档 的一部分。这里使用的技巧很像 C 语言中的 #include 系统——我 们要求 make 把这个文件 include
    到 makefile 里,如下:
    include depends
    GNU Make 看到这个,检查
    'depends' 目的是否更新了,如果没有, 它用我们给它的命令重新产生 depends 档。然后它会把这组(新)
    规则包含进来,继续处理最终目标 'myprog' 。当看到有关 myprog 的规则,它会检查所有的目标文件是否更新——利用 depends
    文件 里的规则,当然这些规则现在已经是更新过的了。
    这个系统其实效率很低,因为每当一个源码文件被改动,所有的源码 文件都要被预处理以产生一个新的 'depends' 文件。而且它也不是 100% 的安全,这是因为当一个 header 档被改动,依靠信息并不会 被更新。但就基本工作来说,它也算相当有用的了。
    2.8 一个更好的 makefile

    是一个我为我大多数项目设计的 makefile 。它应该可以不需要修 改的用在大部分项目里。我主要把它用在 djgpp 上,那是一个 DOS
    版的 gcc 编译器。因此你可以看到执行的命令名、 'alleg' 程序包、 和 RM -F 变量都反映了这一点。
    === makefile 开始 ===
    ######################################
    #
    # Generic makefile
    #
    # by George Foot
    # email: george.foot@merton.ox.ac.uk
    #
    # Copyright (c) 1997 George Foot
    # All rights reserved.
    # 保留所有版权
    #
    # No warranty, no liability;
    # you use this at your own risk.
    # 没保险,不负责
    # 你要用这个,你自己担风险
    #
    # You are free to modify and
    # distribute this without giving
    # credit to the original author.
    # 你可以随便更改和散发这个文件
    # 而不需要给原作者什么荣誉。
    # (你好意思?)
    #
    ######################################
    ### Customising
    # 用户设定
    #
    # Adjust the following if necessary; EXECUTABLE is the target
    # executable's filename, and LIBS is a list of libraries to link in
    # (e.g. alleg, stdcx, iostr, etc). You can override these on make's
    # command line of course, if you prefer to do it that way.
    #
    # 如果需要,调整下面的东西。 EXECUTABLE 是目标的可执行文件名, LIBS
    # 是一个需要连接的程序包列表(例如 alleg, stdcx, iostr 等等)。当然你
    # 可以在 make 的命令行覆盖它们,你愿意就没问题。
    #
    EXECUTABLE := mushroom.exe
    LIBS := alleg
    # Now alter any implicit rules' variables if you like, e.g.:
    #
    # 现在来改变任何你想改动的隐含规则中的变量,例如
    CFLAGS := -g -Wall -O3 -m486
    CXXFLAGS := $(CFLAGS)
    # The next bit checks to see whether rm is in your djgpp bin
    # directory; if not it uses del instead, but this can cause (harmless)
    # `File not found' error messages. If you are not using DOS at all,
    # set the variable to something which will unquestioningly remove
    # files.
    #
    # 下面先检查你的 djgpp 命令目录下有没有 rm 命令,如果没有,我们使用
    # del 命令来代替,但有可能给我们 'File not found' 这个错误信息,这没
    # 什么大碍。如果你不是用 DOS ,把它设定成一个删文件而不废话的命令。
    # (其实这一步在 UNIX 类的系统上是多余的,只是方便 DOS 用户。 UNIX
    # 用户可以删除这5行命令。)
    ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)
    RM-F := rm -f
    else
    RM-F := del
    endif
    # You shouldn't need to change anything below this point.
    #
    # 从这里开始,你应该不需要改动任何东西。(我是不太相信,太NB了!)
    SOURCE := $(wildcard *.c) $(wildcard *.cc)
    OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))
    DEPS := $(patsubst %.o,%.d,$(OBJS))
    MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
    MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS))  
    $(patsubst %.d,%.cc,$(MISSING_DEPS)))
    CPPFLAGS += -MD
    .PHONY : everything deps objs clean veryclean rebuild
    everything : $(EXECUTABLE)
    deps : $(DEPS)
    objs : $(OBJS)
    clean :
    @$(RM-F) *.o
    @$(RM-F) *.d
    veryclean: clean
    @$(RM-F) $(EXECUTABLE)
    rebuild: veryclean everything
    ifneq ($(MISSING_DEPS),)
    $(MISSING_DEPS) :
    @$(RM-F) $(patsubst %.d,%.o,$@)
    endif
    -include $(DEPS)
    $(EXECUTABLE) : $(OBJS)
    gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))
    === makefile 结束 ===
    有几个地方值得解释一下的。首先,我在定义大部分变量的时候使 用的是 := 而不是 = 符号。它的作用是立即把定义中参考到的函数和变量都展开了。如果使用 = 的话,函数和变量参考会留在那 儿,就是说改变一个变量的值会导致其它变量的值也被改变。例 如:
    A = foo
    B = $(A)
    # 现在 B 是 $(A) ,而 $(A) 是 'foo' 。
    A = bar
    # 现在 B 仍然是 $(A) ,但它的值已随着变成 'bar' 了。
    B := $(A)
    # 现在 B 的值是 'bar' 。
    A = foo
    # B 的值仍然是 'bar' 。
    make 会忽略在 # 符号后面直到那一行结束的所有文字。
    ifneg...else...endif
    系统是 makefile 里让某一部分码有条件的 失效/有效的工具。 ifeq 使用两个参数,如果它们相同,它把直 到 else (或者
    endif ,如果没有 else 的话)的一段码加进 makefile 里;如果不同,把 else 到 endif 间的一段码加入
    makefile (如果有 else )。 ifneq 的用法刚好相反。
    'filter-out' 函数使用两个用空格分开的列表,它把第二列表中所 有的存在于第一列表中的项目删除。我用它来处理 DEPS 列表,把所 有已经存在的项目都删除,而只保留缺少的那些。

    前面说过, CPPFLAGS 存有用于隐含规则中传给预处理器的一些 旗标。而 -MD 开关类似 -M 开关,但是从源码文件 .c 或 .cc
    中 形成的文件名是使用后缀 .d 的(这就解释了我形成 DEPS 变量的 步骤)。DEPS 里提到的文件后来用 '-include' 加进了
    makefile 里,它隐藏了所有因文件不存在而产生的错误信息。
    如果任何依靠文件不存在, makefile 会把相应的 .o 文件从磁碟 上删除,从而使得 make 重建它。因为 CPPFLAGS 指定了 -MD , 它的 .d 文件也被重新产生。
    最后, 'addprefix' 函数把第二个参数列表的每一项前缀上第一 个参数值。
    这个 makefile 的那些目的是(这些目的可以传给 make 的命令行 来直接选用):
    everything:(预设) 更新主要的可执行程序,并且为每一个 源码文件生成或更新一个 '.d' 文件和一个 '.o' 文件。
    deps: 只是为每一个源码程序产生或更新一个 '.d' 文件。
    objs: 为每一个源码程序生成或更新 '.d' 文件和目标文件。
    clean: 删除所有中介/依靠文件( *.d 和 *.o )。
    veryclean: 做 `clean' 和删除可执行文件。
    rebuild: 先做 `veryclean' 然后 `everything' ;既完全重建。
    除了预设的 everything 以外,这里头只有 clean , veryclean , 和 rebuild 对用户是有意义的。

    还没有发现当给出一个源码文件的目录,这个 makefile 会失败的 情况,除非依靠文件被弄乱。如果这种弄乱的情况发生了,只要输入 `make
    clean' ,所有的目标文件和依靠文件会被删除,问题就应该 被解决了。当然,最好不要把它们弄乱。如果你发现在某种情况下这 个
    makefile 文件不能完成它的工作,请告诉我,我会把它整好的。
    3 总结
    ~~~~~~~~~~~~~~~
    我希望这篇文章足够详细的解释了多文件项目是怎么运作的,也说明了 怎样安全而合理的使用它。到此,你应该可以轻松的利用 GNU Make 工 具来管理小型的项目,如果你完全理解了后面几个部分的话,这些对于 你来说应该没什么困难。
    GNU
    Make 是一件强大的工具,虽然它主要是用来建立程序,它还有很多
    别的用处。如果想要知道更多有关这个工具的知识,它的句法,函数,和许多别的特点,你应该参看它的参考文件 (info pages, 别的 GNU
    工具也一样,看它们的 info pages. )。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    find与xargs
    来源: Blog.ChinaUnix.net
    http://www.it208.com/list1/Shell/200511/113468.html
    http://www.it208.com/list1/Shell/200511/113467.html
    使用find命令的-exec选项处理匹配到的文件时,find命令将所有匹配到的文件一起传递给exec执行。不幸的是,有些系统对能够传递给exec
    的命令长度有限制,这样在find命令运行几分钟之后,就会出现溢出错误。错误信息通常是“参数列太长”或“参数列溢出”。这就是xargs命令的用处所
    在,特别是与find命令一起使用。Find命令把匹配到的文件传递给xargs命令,而xargs命令每次只获取一部分文件而不是全部,不像-exec
    选项那样。这样它可以先处理最先获取的一部分文件,然后是下一批,并如此继续下去。在有些系统中,使用-exec选项会为处理每一个匹配到的文件而发起一
    个相应的进程,并非将匹配到的文件全部作为参数一次执行;这样在有些情况下就会出现进程过多,系统性能下降的问题,因而效率不高;而使用xargs命令则
    只有一个进程。另外,在使用x a rg
    s命令时,究竟是一次获取所有的参数,还是分批取得参数,以及每一次获取参数的数目都会根据该命令的选项及系统内核中相应的可调参数来确定。
    看看xargs命令是如何同find命令一起使用的,以下是一些例子。
    下面的例子在整个系统中查找内存信息转储文件(core dump) ,然后把结果保存到/tmp/core.log 文件中:
    $ find . -name "core" -print | xargs echo "" >/tmp/core.log
    下面的例子在/apps/audit目录下查找所有用户具有读、写和执行权限的文件,并收回相应的写权限:
    $ find /apps/audit -perm -7 -print | xargs chmod o-w
    在下面的例子中,我们用grep命令在所有的普通文件中搜索device这个词:
    $ find / -type f -print | xargs grep "device"
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    Makefile初探
    原文出自:http://www.linuxforum.net  
    作者:jkl
      
    Linux
    的内核配置文件有两个,一个是隐含的.config文件,嵌入到主Makefile中;另一个是include/linux/autoconf.h,嵌入
    到各个c源文件中,它们由make config、make menuconfig、make
    xconfig这些过程创建。几乎所有的源文件都会通过linux/config.h而嵌入autoconf.h,如果按照通常方法建立文件依赖关系
    (.depend),只要更新过autoconf.h,就会造成所有源代码的重新编绎。
    为了优化make过程,减少不必要的重新编
    绎,Linux开发了专用的mkdep工具,用它来取代gcc来生成.depend文件。mkdep在处理源文件时,忽略linux/config.h这
    样的头文件,识别源文件宏指令中具有"CONFIG_"特征的行。例如,如果有"#ifdef
    CONFIG_SMP"这样的行,它就会在.depend文件中输出$(wildcard
    /usr/src/linux/include/config/smp.h)。
    include/config/下的文件是另一个工具
    split-include从autoconf.h中生成,它利用autoconf.h中的CONFIG_标记,生成与mkdep相对应的文件。例如,如
    果autoconf.h中有"#undef
    CONFIG_SMP"这一行,它就生成include/config/smp.h文件,内容为"#undef
    CONFIG_SMP"。这些文件名只在.depend文件中出现,内核源文件是不会嵌入它们的。每配置一次内核,运行split-include一次。
    split-include会检查旧的子文件的内容,确定是不是要更新它们。这样,不管autoconf.h修改日期如何,只要其配置不变,make就不
    会重新编绎内核。
    如果系统的编绎选项发生了变化,Linux也能进行增量编绎。为了做到这一点,make每编绎一个源文件时生成一个
    flags文件。例如编绎sched.c时,会在相同的目录下生成隐含的.sched.o.flags文件。它是Makefile的一个片断,当make
    进入某个子目录编绎时,会搜索其中的flags文件,将它们嵌入到Makefile中。这些flags代码测试当前的编绎选项与原来的是不是相同,如果相
    同,就将自已对应的目标文件加入FILES_FLAGS_UP_TO_DATE列表,然后,系统从编绎对象表中删除它们,得到
    FILES_FLAGS_CHANGED列表,最后,将它们设为目标进行更新。
    下一步准备逐步深入的剖析Makefile代码。
    ==========================================
    Makefile解读之二: sub-make  
    ==========================================
    Linux
    各级内核源代码的子目录下都有Makefile,大多数Makefile要嵌入主目录下的Rule.make,Rule.make将识别各个
    Makefile中所定义的一些变量。变量obj-y表示需要编绎到内核中的目标文件名集合,定义O_TARGET表示将obj-y连接为一个
    O_TARGET名称的目标文件,定义L_TARGET表示将obj-y合并为一个L_TARGET名称的库文件。同样obj-m表示需要编绎成模块的目
    标文件名集合。如果还需进行子目录make,则需要定义subdir-y和subdir-m。在Makefile中,用"obj-$
    (CONFIG_BINFMT_ELF) += binfmt_elf.o"和"subdir-$(CONFIG_EXT2_FS) +=
    ext2"这种形式自动为obj-y、obj-m、subdir-y、subdir-m添加文件名。有时,情况没有这么单纯,还需要使用条件语句个别对
    待。Makefile中还有其它一些变量,如mod-subdirs定义了subdir-m以外的所有模块子目录。
    Rules.make
    是如何使make进入子目录的呢?
    先来看subdir-y是如何处理的,在Rules.make中,先对subdir-y中的每一个文件名加上前缀"_subdir_"再进行排序生成
    subdir-list集合,再以它作为目标集,对其中每一个目标产生一个子make,同时将目标名的前缀去掉得到子目录名,作为子make的起始目录参
    数。subdir-m与subdir-y类似,但情况稍微复杂一些。由于subdir-y中可能有模块定义,因此利用mod-subdirs变量将
    subdir-y中模块目录提取出来,再与subdir-m合成一个大的MOD_SUB_DIRS集合。subdir-m的目标所用的前缀是
    "_modsubdir_"。
    一点说明,子目录中的Makefile与Rules.make都没有嵌入.config文件,它是通过
    主Makefile向下传递MAKEFILES变量完成的。MAKEFILES是make自已识别的一个变量,在执行新的Makefile之前,make
    会首先加载MAKEFILES所指的文件。在主Makefile中它即指向.config。
    ==========================================
    Makefile解读之三: 模块的版本化处理
    ==========================================

    块的版本化是内核与模块接口之间进行严格类型匹配的一种方法。当内核配置了CONFIG_MODVERSIONS之后,make
    dep操作会在include/linux/modules/目录下为各级Makefile中export-objs变量所对应的源文件生成扩展名为.
    ver的文件。
    例如对于kernel/ksyms.c,make用以下命令生成对应的ksyms.ver:
    gcc -E -D__KERNEL__ -D__GENKSYMS__ ksyms.c | /sbin/genksyms -k 2.4.1 > ksyms.ver
    -D__GENKSYMS__的作用是使ksyms.c中的EXPORT_SYMBOL宏不进行扩展。genksyms命令识别EXPORT_SYMBOL()中的函数名和对应的原型,再根据其原型计算出该函数的版本号。
    例如ksyms.c中有一行:
    EXPORT_SYMBOL(kmalloc);
    kmalloc原型是:
    void *kmalloc(size_t, int);
    genksyms程序对应的输出为:
    #define __ver_kmalloc 93d4cfe6
    #define kmalloc _set_ver(kmalloc)
    在内核符号表和模块中,kmalloc将变成kmalloc_R93d4cfe6。

    生成完所有的.ver文件后,make将重建include/linux/modversions.h文件,它包含一系列#include指令行嵌入各
    个.ver文件。在编绎内核本身export-objs中的文件时,make会增加一个"-DEXPORT_SYMTAB"编绎标志,它使源文件嵌入
    modversions.h文件,将EXPORT_SYMBOL宏展开中的函数名字符串进行版本名扩展;同时,它也定义_set_ver()宏为一空操
    作,使代码中的函数名不受其影响。
    在编绎模块时,make会增加"-include=linux/modversion.h -DMODVERSIONS"编绎标志,使模块中代码的函数名得到相应版本扩展。

    于生成.ver文件比较费时,make还为每个.ver创建了一个后缀为.stamp时戳文件。在make
    dep时,如果其.stamp文件比源文件旧才重新生成.ver文件,否则只是更新.stamp文件时戳。另外,在生成.ver和
    modversions.h文件时,make都会比较新文件和旧文件的内容,保持它们修改时间为最旧。
    ==========================================
    Makefile解读之四: Rules.make的注释
    ==========================================
    [code:1:974578564b]
    #
    # This file contains rules which are shared between multiple Makefiles.
    #
    #
    # False targets.
    #
    #  
    .PHONY: dummy  
    #
    # Special variables which should not be exported
    #
    # 取消这些变量通过环境向make子进程传递。
    unexport EXTRA_AFLAGS # as 的开关
    unexport EXTRA_CFLAGS # cc 的开关
    unexport EXTRA_LDFLAGS  # ld 的开关
    unexport EXTRA_ARFLAGS # ar 的开关
    unexport SUBDIRS #  
    unexport SUB_DIRS # 编绎内核需进入的子目录,等于subdir-y
    unexport ALL_SUB_DIRS # 所有的子目录
    unexport MOD_SUB_DIRS # 编绎模块需进入的子目录
    unexport O_TARGET # ld合并的输出对象
    unexport ALL_MOBJS # 所有的模块名
    unexport obj-y # 编绎成内核的文件集
    unexport obj-m # 编绎成模块的文件集
    unexport obj-n #  
    unexport obj- #  
    unexport export-objs # 需进行版本处理的文件集
    unexport subdir-y # 编绎内核所需进入的子目录
    unexport subdir-m # 编绎模块所需进入的子目录
    unexport subdir-n
    unexport subdir-
    #
    # Get things started.
    #
    first_rule: sub_dirs
    $(MAKE) all_targets
    # 在内核编绎子目录中过滤出可以作为模块的子目录。
    both-m          := $(filter $(mod-subdirs), $(subdir-y))  
    SUB_DIRS := $(subdir-y)
    # 求出总模块子目录
    MOD_SUB_DIRS := $(sort $(subdir-m) $(both-m))
    # 求出总子目录
    ALL_SUB_DIRS := $(sort $(subdir-y) $(subdir-m) $(subdir-n) $(subdir-))
    #
    # Common rules
    #
    # 将c文件编绎成汇编文件的规则,$@为目标对象。
    %.s: %.c
    $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -S $ $@
    # 将c文件编绎成目标文件的规则,$ $(dir $@)/.$(notdir $@).flags
    # 汇编文件生成目标文件的规则。
    %.o: %.s
    $(AS) $(AFLAGS) $(EXTRA_CFLAGS) -o $@ $ $@
    # 汇编文件生成目标文件的标准规则。
    %.o: %.S
    $(CC) $(AFLAGS) $(EXTRA_AFLAGS) $(AFLAGS_$@) -c -o $@ $ $(dir $@)/.$(notdir $@).flags
    endif # O_TARGET
    #
    # Rule to compile a set of .o files into one .a file
    #
    # 将obj-y组合成库L_TARGET的方法。
    ifdef L_TARGET
    $(L_TARGET): $(obj-y)
    rm -f $@
    $(AR) $(EXTRA_ARFLAGS) rcs $@ $(obj-y)
    @ (  
        echo 'ifeq ($(strip $(subst $(comma),:,$(EXTRA_ARFLAGS)
    $(obj-y))),$$(strip $$(subst $$(comma),:,$$(EXTRA_ARFLAGS) $$(obj-y))))' ;

        echo 'FILES_FLAGS_UP_TO_DATE += $@' ;  
        echo 'endif'  
    ) > $(dir $@)/.$(notdir $@).flags
    endif
    #
    # This make dependencies quickly
    #
    # wildcard为查找目录中的文件名的宏。
    fastdep: dummy  
    $(TOPDIR)/scripts/mkdep $(wildcard *.[chS] local.h.master) > .depend
    ifdef ALL_SUB_DIRS
    #
    将ALL_SUB_DIRS中的目录名加上前缀_sfdep_作为目标运行子make,并将ALL_SUB_DIRS
    通过
    # 变量_FASTDEP_ALL_SUB_DIRS传递给子make。
    $(MAKE) $(patsubst %,_sfdep_%,$(ALL_SUB_DIRS))
    _FASTDEP_ALL_SUB_DIRS="$(ALL_SUB_DIRS)"
    endif
    ifdef _FASTDEP_ALL_SUB_DIRS
    #
    与上一段相对应,定义子目录目标,并将目标名还原为目录名,进入该子目录make。
    $(patsubst %,_sfdep_%,$(_FASTDEP_ALL_SUB_DIRS)):
    $(MAKE) -C $(patsubst _sfdep_%,%,$@) fastdep
    endif
    #
    # A rule to make subdirectories
    #
    # 下面2段完成内核编绎子目录中的make。
    subdir-list = $(sort $(patsubst %,_subdir_%,$(SUB_DIRS)))
    sub_dirs: dummy $(subdir-list)
    ifdef SUB_DIRS
    $(subdir-list) : dummy
    $(MAKE) -C $(patsubst _subdir_%,%,$@)
    endif
    #
    # A rule to make modules
    #
    # 求出有效的模块文件表。
    ALL_MOBJS = $(filter-out $(obj-y), $(obj-m))
    ifneq "$(strip $(ALL_MOBJS))" ""
    # 取主目录TOPDIR到当前目录的路径。
    PDWN=$(shell $(CONFIG_SHELL) $(TOPDIR)/scripts/pathdown.sh)
    endif
    unexport MOD_DIRS
    MOD_DIRS := $(MOD_SUB_DIRS) $(MOD_IN_SUB_DIRS)
    # 编绎模块时,进入模块子目录的方法。
    ifneq "$(strip $(MOD_DIRS))" ""
    .PHONY: $(patsubst %,_modsubdir_%,$(MOD_DIRS))
    $(patsubst %,_modsubdir_%,$(MOD_DIRS)) : dummy
    $(MAKE) -C $(patsubst _modsubdir_%,%,$@) modules
    # 安装模块时,进入模块子目录的方法。
    .PHONY: $(patsubst %,_modinst_%,$(MOD_DIRS))
    $(patsubst %,_modinst_%,$(MOD_DIRS)) : dummy
    $(MAKE) -C $(patsubst _modinst_%,%,$@) modules_install
    endif
    # make modules 的入口。
    .PHONY: modules
    modules: $(ALL_MOBJS) dummy  
    $(patsubst %,_modsubdir_%,$(MOD_DIRS))
    .PHONY: _modinst__
    # 拷贝模块的过程。
    _modinst__: dummy
    ifneq "$(strip $(ALL_MOBJS))" ""
    mkdir -p $(MODLIB)/kernel/$(PDWN)
    cp $(ALL_MOBJS) $(MODLIB)/kernel/$(PDWN)
    endif
    # make modules_install 的入口,进入子目录安装。
    .PHONY: modules_install
    modules_install: _modinst__  
    $(patsubst %,_modinst_%,$(MOD_DIRS))
    #
    # A rule to do nothing
    #
    dummy:
    #
    # This is useful for testing
    #
    script:
    $(SCRIPT)
    #
    # This sets version suffixes on exported symbols
    # Separate the object into "normal" objects and "exporting" objects
    # Exporting objects are: all objects that define symbol tables
    #
    ifdef CONFIG_MODULES
    # list-multi列出那些由多个文件复合而成的模块;
    # 从编绎文件表和模块文件表中过滤出复合模块名。
    multi-used := $(filter $(list-multi), $(obj-y) $(obj-m))
    # 取复合模块的构成表。
    multi-objs := $(foreach m, $(multi-used), $($(basename $(m))-objs))
    # 求出需进行编译的总模块表。
    active-objs := $(sort $(multi-objs) $(obj-y) $(obj-m))
    ifdef CONFIG_MODVERSIONS
    ifneq "$(strip $(export-objs))" ""  
    # 如果有需要进行版本化的文件。
    MODINCL = $(TOPDIR)/include/linux/modules
    # The -w option (enable warnings) for genksyms will return here in 2.1
    # So where has it gone?
    #
    # Added the SMP separator to stop module accidents between uniprocessor
    # and SMP Intel boxes - AC - from bits by Michael Chastain
    #
    ifdef CONFIG_SMP
    genksyms_smp_prefix := -p smp_
    else
    genksyms_smp_prefix :=  
    endif
    # 从源文件计算版本文件的规则。
    $(MODINCL)/%.ver: %.c
    @if [ ! -r $(MODINCL)/$*.stamp -o $(MODINCL)/$*.stamp -ot $ $@.tmp';  
    $(CC) $(CFLAGS) -E -D__GENKSYMS__ $ $@.tmp;  
    if [ -r $@ ] && cmp -s $@ $@.tmp; then echo $@ is unchanged; rm -f
    $@.tmp;  
    else echo mv $@.tmp $@; mv -f $@.tmp $@; fi;  
    fi; touch $(MODINCL)/$*.stamp
    #
    将版本处理源文件的扩展名改为.ver,并加上完整的路径名,它们依赖于autoconf.h?br>?br>$(addprefix $(MODINCL)/,$(export-objs:.o=.ver)):
    $(TOPDIR)/include/linux/autoconf.h
    # updates .ver files but not modversions.h
    # 通过fastdep,逐个生成export-objs对应的版本文件。
    fastdep: $(addprefix $(MODINCL)/,$(export-objs:.o=.ver))
    # updates .ver files and modversions.h like before (is this needed?)
    # make dep过程的入口
    dep: fastdep update-modverfile
    endif # export-objs  
    # update modversions.h, but only if it would change
    # 刷新版本文件的过程。
    update-modverfile:
    @(echo "#ifndef _LINUX_MODVERSIONS_H";
      echo "#define _LINUX_MODVERSIONS_H";  
      echo "#include ";  
      cd $(TOPDIR)/include/linux/modules;  
      for f in *.ver; do  
        if [ -f $$f ]; then echo "#include "; fi;  
      done;  
      echo "#endif";  
    ) > $(TOPDIR)/include/linux/modversions.h.tmp
    @if [ -r $(TOPDIR)/include/linux/modversions.h ] && cmp -s
    $(TOPDIR)/include/linux/modversions.h
    $(TOPDIR)/include/linux/modversions.h.tmp; then  
    echo $(TOPDIR)/include/linux/modversions.h was not updated;  
    rm -f $(TOPDIR)/include/linux/modversions.h.tmp;  
    else  
    echo $(TOPDIR)/include/linux/modversions.h was updated;  
    mv -f $(TOPDIR)/include/linux/modversions.h.tmp
    $(TOPDIR)/include/linux/modversions.h;  
    fi
    $(active-objs): $(TOPDIR)/include/linux/modversions.h
    else
    # 如果没有配置版本化,modversions.h的内容。
    $(TOPDIR)/include/linux/modversions.h:
    @echo "#include " > $@
    endif # CONFIG_MODVERSIONS
    ifneq "$(strip $(export-objs))" ""
    # 版本化目标文件的编绎方法。
    $(export-objs): $(export-objs:.o=.c) $(TOPDIR)/include/linux/modversions.h
    $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -DEXPORT_SYMTAB -c $(@:.o=.c)
    @ (  
        echo 'ifeq ($(strip $(subst $(comma),:,$(CFLAGS) $(EXTRA_CFLAGS)
    $(CFLAGS_$@) -DEXPORT_SYMTAB)),$$(strip $$(subst $$(comma),:,$$(CFLAGS)
    $$(EXTRA_CFLAGS) $$(CFLAGS_$@) -DEXPORT_SYMTAB)))' ;  
        echo 'FILES_FLAGS_UP_TO_DATE += $@' ;  
        echo 'endif'  
    ) > $(dir $@)/.$(notdir $@).flags
    endif
    endif # CONFIG_MODULES
    #
    # include dependency files if they exist
    #
    # 嵌入源文件之间的依赖关系。
    ifneq ($(wildcard .depend),)
    include .depend
    endif
    # 嵌入头文件之间的依赖关系。
    ifneq ($(wildcard $(TOPDIR)/.hdepend),)
    include $(TOPDIR)/.hdepend
    endif
    #
    # Find files whose flags have changed and force recompilation.
    # For safety, this works in the converse direction:
    #   every file is forced, except those whose flags are positively
    up-to-date.
    #
    # 已经更新过的文件列表。
    FILES_FLAGS_UP_TO_DATE :=  
    # For use in expunging commas from flags, which mung our checking.
    comma = ,
    # 将当前目录下所有flags文件嵌入。
    FILES_FLAGS_EXIST := $(wildcard .*.flags)
    ifneq ($(FILES_FLAGS_EXIST),)
    include $(FILES_FLAGS_EXIST)
    endif
    # 将无需更新的文件从总的对象中删除。
    FILES_FLAGS_CHANGED := $(strip  
        $(filter-out $(FILES_FLAGS_UP_TO_DATE),  
    $(O_TARGET) $(L_TARGET) $(active-objs)  
    ))
    # A kludge: .S files don't get flag dependencies (yet),
    #   because that will involve changing a lot of Makefiles.  Also
    #   suppress object files explicitly listed in $(IGNORE_FLAGS_OBJS).
    #   This allows handling of assembly files that get translated into
    #   multiple object files (see arch/ia64/lib/idiv.S, for example).
    #  
    # 将由汇编文件生成的目件文件从FILES_FLAGS_CHANGED删除。
    FILES_FLAGS_CHANGED := $(strip  
        $(filter-out $(patsubst %.S, %.o, $(wildcard *.S)
    $(IGNORE_FLAGS_OBJS)),  
        $(FILES_FLAGS_CHANGED)))
    # 将FILES_FLAGS_CHANGED设为目标。
    ifneq ($(FILES_FLAGS_CHANGED),)
    $(FILES_FLAGS_CHANGED): dummy
    endif
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    Linux Kernel Makefiles
    摘自
    http://www.wonyen.net/bbs/topic.aspx?id=7008
    1、概述
    Makefile 由五个部分组成:
    Makefile:顶层 Makefile。
    .config:内核配置文件。
    arch/*/Makefile:体系结构 Makefiles。
    子目录 Makefile:大约三百个。
    Rules.make:为所有子目录 Makefile 提供通用规则。
    顶层 Makefile 读入在内核配置过程中生成的 .config 文件。
    顶层 Makefile 负责两个主要产品的创建:vminux (常驻内核映象) 和模块 (任何模块文件)。它通过递归下降到内核源代码树以创建这些目标。需要进入的子目录由内核配置确定。
    顶层 Makefile 引入一个名为 arch/$(ARCH)/Makefile 的体系结构 Makefile。体系结构 Makefile 为顶层 Makefile 提供体系结构特定的信息。
    每一子目录都有一个 Makefile 以完成从上层传递来的命令。子目录 Makefile 使用来自 .config 文件的信息以构造各种文件列表,而后引入 Rules.make 中的通用规则。
    Rules.make 定义了所有子目录 Makefile 的通用规则。它的一个变量列表组成了它的公共界面。而后它根据这些列表声明规则。
    2、谁需要什么
    人们跟内核的 Makefile 有四种不同的关系。
    用户是创建内核的人。这些人输入诸如“make menuconfig”或 “make bzImage”之类的命令。他们通常并不阅读或编辑内核 Makefile (或任何其它源代码)。

    通开发者是开发诸如设备驱动程序、文件系统或网络协议的人。这些人需要为他们开发的子系统维护用于该子系统的子目录
    Makefile。为了有效地完成这一维护任务,他们需要一些关于内核 Makefile 的全局性的知识,以及一些关于 Rules.make
    公共界面的细节。
    体系结构开发者是开发整个体系结构 (例如 spare 或 ia64) 的人。体系结构开发者需要理解体系结构 Makefile 以及子目录 Makefile。
    Kbuild 开发者是开发内核创建系统本身的人。这些人需要知道内核 Makefile 的所有方面。
    本文档是面向普通开发者和体系结构开发者的。
    3、Makefile 语言
    内核 Makefile 被设计为使用 GNU Make。这些 Makefiles 只使用 GNU Make 文档说明了的功能,但它们使用了很多 GNU 扩展。
    GNU Make 支持基本列表处理函数。内核 Makefile 使用带有少量 if 语句的新颖的列表创建和操作风格。
    GNU Make 有两种赋值操作符:“:=”和“。“: ”立即对右侧进行求值并且将结果字符串存储于左侧变量中。“=”更像公式定义;它以未求值形式保存右侧的内容并在使用左侧变量的时候对此内容进行求值。
    在有些情况下“=”比较适用。但通常“:=”是正确的选择。
    本文档的所有示例都来自于实际的内核源代码。这些示例都被重新排版过 (改变了空白符和行的划分),但其它都完全一致。
    4、从顶层传递下去的变量
    顶层 Makefile 导出以下变量:  VERSION、 PATCHLEVEL、SUBLEVEL、EXTRAVERSION
    这些变量定义了当前内核版本。少数体系结构 Makefile 直接使用这些值;它们应该使用 $(KERNELRELEASE)。
    $(VERSION)、$(PATCHLEVEL) 和 $(SUBLEVEL) 定义了三个基本版本编号,例如“2”、“4”和“0”。这三个值总是数值。
    $(EXTRAVERSION) 定义了更低级别的预备补丁或附加补丁。它通常是类似于 “-pre4的”非数值字符串,并且往往为空。
    KERNELRELEASE
    $(KERNELRELEASE) 是类似于“2.4.0-pre4”的单个字符串,适于构造安装目录名或在版本字符串中显示。某些体系结构 Makefile 将它用于这样的目的。
    ARCH
    该变量定义了目标体系结构,例如“i386”、“arm”或 “sparc”。许多子目录 Makefile 测试 $(ARCH) 以决定编译那些文件。
    在默认情况下,顶层 Makefile 将 $(ARCH) 设定为主机系统的体系机构。为了进行交叉创建,用户可以在命令行覆盖 $(ARCH) 的值。
    make ARCH=m68k ...

    TOPDIR、HPATH
    $(TOPDIR) 是内核源代码树顶层目录的路径。子目录 Makefile 需要此变量以便引入 $(TOPDIR)/Rules.make。
    $(HPATH) 等价于 $(TOPDIR)/include。少数体系结构 Makefile 需要它以使用引入文件做一些特殊的事。
    SUBDIRS
    $(SUBDIRS) 是顶层 Makefile 应该进入以便完成 vmlinux 或模块创建的目录列表。$(SUBDIRS) 含有那些目录取决于内核配置。顶层 Makefile 定义此变量,体系结构Makefile 扩展此变量。
    HEAD、CORE_FILES、NETWORKS、DRIVERS、LIBS、LINKFLAGS
    $(HEAD)、$(CORE_FILES)、$(NETWORKS)、$(DRIVERS) 和 $(LIBS) 给出要连接到 vmlinux 中的目标文件和库的列表。
    $(HEAD) 中的文件首先连接到 vmlinux 中。
    $(LINKFLAGS) 指定创建 vmlinux 的标志。

    层 Makefile 和体系结构 Makefile 联合定义这些变量。顶层 Makefile 定义
    $(CORE_FILES)、$(NETWORKS)、$(DRIVERS) 和 $(LIBS)。体系结构 Makefile 定义 $(HEAD)
    和 $(LINKFLAGS) 并扩展 $(CORE_FILES) 和 $(LIBS)。
    注意:这些变量并不都是必需的。$(NETWORKS)、$(DRIVERS)甚至 $(LIBS) 都应该合并到 $(CORE_FILES) 中去。
    CPP、CC、AS、LD、AR、NM、STRIP、OBJCOPY、OBJDUMP、CPPFLAGS、CFLAGS、CFLAGS_KERNEL、MODFLAGS、AFLAGS、LDFLAGS、PERL、GENKSYMS
    这些变量指定了 Rules.make 用于从源代码文件创建目标文件的命令和标志。
    $(CFLAGS_KERNEL) 含有用于编译常驻内核代码的附加 C 编译器标志。
    $(MODFLAGS) 含有用于编译可载入内核模块的附加 C 编译器标志。将来该标志可能要改为更通用的 $(CFLAGS_MODULE)。
    $(AFLAGS) 含有汇编标志。
    $(GENKSYMS) 含有用于生成在启用了 CONFIG_MODVERSIONS 时内核符号签名的命令。 genksyms 命令由 modutils 包提供。
    CROSS_COMPILE
    该变量是诸如 $(CC)、$(AS) 和 $(LD) 之类的其它变量的前缀路径。体系结构Makefile 有时显式使用并设置该变量。子目录 Makefile 不需要关心它。
    如果需要,用户可以在命令行中覆盖 $(CROSS_COMPILE) 的值。
    HOSTCC、HOSTCFLAGS
    这些变量定义了用于编译在本地主机运行的程序的 C 编译器和 C 编译器标志。它们使用单独的变量是因为目标体系结构可能于主机体系结构不同。
    如果您的 Makefile 编译并运行一个在创建内核的过程中运行的程序,那它就应该使用 $(HOSTCC) 和 $(HOSTCFLAGS)。
    例如,drivers/pci 子目录含有名为 gen-devlist.c 的助手程序。该程序读入一个 PCI ID 列表并生成名为 classlist.h 和 devlist.h 的 C 代码。
    假定用户有一台 i386 计算机并需要创建一个用于 ia64 机器的内核。那么用户就应该在大多数编译中使用 ia64 交叉编译器,但应该使用 i386 本地编译器编译 drivers/pci/gen-devlist.c。
    还有,诸如 scripts/mkdep.c 和 scripts/lxdialog/*.c 那样的 kbuild 助手程序应该用 $(HOSTCC) 而不是 $(CC) 编译。
    ROOT_DEV、SVGA_MODE、RAMDISK
    最终用户编辑该变量以指定关于他们的内核的特定配置信息。这些变量是古董!他们也只能用于 i386 体系结构。他们实在是应该用 CONFIG_* 选项来替代了。
    MAKEBOOT
    该变量只在顶层 Makefile 中定义和使用。顶层 Makefile 不该导出它。
    INSTALL_PATH
    该变量定义了体系结构 Makefile 应该将常驻内核映象和 System.map 文件安装到那里去。
    INSTALL_MOD_PATH、MODLIB
    $(INSTALL_MOD_PATH) 指定了用于安装模块的 $(MODLIB) 的前缀。 Makefile 没有定义该变量,但如果需要的话,用户可以定义它。
    $(MODLIB) 指定了模块安装的目录。顶层 Makefile 将 $(MODLIB) 定义为 $(INSTALL_MOD_PATH)/lib/modules/$(KERNELRELEASE)。如果需要用户可以在命令行中覆盖这个值。
    CONFIG_SHELL
    该变量是 Makefile 和 Rules.make 间私有的变量。体系结构 Makefile 和子目录 Makefile 不应该使用它。
    MODVERFILE
    内部变量。由于它从不在顶层 Makefile 以外使用,所以不必导出它。
    MAKE、MAKEFILES
    一些 GNU Make 的内部变量。
    $(MAKEFILES) 特别用于强制体系结构 Makefile 和子目录 Makefile 读入 $(TOPDIR)/.config 而不必显式地引入它。(这是个实线细节并可以修正)。
    5、体系结构 Makefile 的结构
    5.1、体系结构特定的变量
    顶层 Makefile 引入一个体系结构 Makefile:arch/$(ARCH)/Makefile。本节解说体系结构 Makefile 的功能。
    体系结构 Makefile 用体系结构特定的至扩展了某些顶层 Makefile 的变量。  SUBDIRS
    顶层 Makefile 定义 $(SUBDIRS)。体系结构 Makefile 用一组体系结构特定的目录扩展 $(SUBDIRS)。
    例如:
      # arch/alpha/Makefile
      SUBDIRS := $(SUBDIRS) arch/alpha/kernel arch/alpha/mm
                 arch/alpha/lib arch/alpha/math-emu
    该列表可能依赖于配置:
      # arch/arm/Makefile
      ifeq ($(CONFIG_ARCH_ACORN),y)
      SUBDIRS         += drivers/acorn
      ...
      endif

    CPP、CC、AS、LD、AR、NM、STRIP、OBJCOPY、OBJDUMP、CPPFLAGS、CFLAGS、CFLAGS_KERNEL、MODFLAGS、AFLAGS、LDFLAGS
    顶层 Makefile 定义了这些变量,体系结构 Makefile 扩展了它们。
    很多体系结构 Makefile 动态地运行目标 C 编译器以探测它所支持的选项:
      # arch/i386/Makefile
      # prevent gcc from keeping the stack 16 byte aligned
      CFLAGS += $(shell if $(CC) -mpreferred-stack-boundary=2
         -S -o /dev/null -xc /dev/null >/dev/null 2>&1;
         then echo "-mpreferred-stack-boundary=2"; fi)
    而且,$(CFLAGS) 当然可能依赖于配置:
      # arch/i386/Makefile
      ifdef CONFIG_M386
      CFLAGS += -march=i386
      endif
      ifdef CONFIG_M486
      CFLAGS += -march=i486
      endif
      ifdef CONFIG_M586
      CFLAGS += -march=i586
      endif
    某些体系结构 Makefile 重新定义了编译命令以添加体系结构特定的标志:
      # arch/s390/Makefile
      LD=$(CROSS_COMPILE)ld -m elf_s390
      OBJCOPY=$(CROSS_COMPILE)objcopy -O binary -R .note -R .comment -S

    5.2、vmlinux 创建变量
    体系结构 Makefile 和顶层 Makefile 合作以定义确定了如何创建 vmlinux 文件的变量。请注意没有对应的体系结构特定的模块部分;模块创建机制跟体系结构是完全独立的。  HEAD、CORE_FILES、LIBS、LINKFLAGS
    顶层 Makefile 定义了这些变量于体系结构独立的内容,而体系结构 Makefile 扩展了它们。请注意体系结构 Makefile 定义 (而不仅仅是扩展了) $(HEAD) 和 $(LINKFLAGS)。
    例如:
      # arch/m68k/Makefile
      ifndef CONFIG_SUN3
      LINKFLAGS = -T $(TOPDIR)/arch/m68k/vmlinux.lds
      else
      LINKFLAGS = -T $(TOPDIR)/arch/m68k/vmlinux-sun3.lds -N
      endif
      ...
      ifndef CONFIG_SUN3
      HEAD := arch/m68k/kernel/head.o
      else
      HEAD := arch/m68k/kernel/sun3-head.o
      endif
      SUBDIRS += arch/m68k/kernel arch/m68k/mm arch/m68k/lib
      CORE_FILES := arch/m68k/kernel/kernel.o arch/m68k/mm/mm.o $(CORE_FILES)
      LIBS += arch/m68k/lib/lib.a

    5.3、后-vmlinux 目标
    体系结构 Makefile 定义了获取 vmlinux 文件、压缩它、以启动代码包装它、并将结果文件复制到某处的目标。这包括各种类型的安装命令。
    这些后-vmlinux 目标在不同体系结构之间并不通用。下面是这些目标和支持它们的体系结构的列表 (来自于内核版本 2.4.0-test6-pre5): balo mips
    bootimage alpha
    bootpfile alpha、ia64
    bzImage i386、m68k
    bzdisk i386
    bzlilo i386
    compressed i386、m68k、mips、mips64、sh
    dasdfmt s390
    Image arm
    image s390
    install arm、i386
    lilo m68k
    msb alpha、ia64
    my-special-boot alpha、ia64
    orionboot mips
    rawboot alpha
    silo s390
    srmboot alpha
    tftpboot.img sparc、sparc64
    vmlinux.64 mips64
    vmlinux.aout sparc64
    zImage arm, i386, m68k, mips, mips64, ppc, sh
    zImage.initrd ppc
    zdisk i386, mips, mips64, sh
    zinstall arm
    zlilo i386
    znetboot.initrd ppc
    5.4、强制性体系结构特定的目标
    体系结构 Makefile 必须定义以下体系结构特定的目标。这些目标为对应的顶层 Makefile 目标提供了体系结构特定的工作: archclean clean
    archdep dep
    archmrproper mrproper
    6、子目录 Makefile 的结构
    子目录 Makefile 有四个部分。
    6.1、注释
    第一部分是注释头。历史上很多匿名人士编辑了内核 Makefile 而没有在头中留下任何修改记录;来自他们的注释将是很有价值的。
    6.2、目标定义
    第二部分是一组作为子目录 Makefile 核心的定义。这些行定义了需要创建的文件、所有特殊的编译选项,以及必须递归进入的子目录。这些行的声明严重地依赖于内核配置变量 (CONFIG_* 符号)。
    第二部分看起来是这样:
    # drivers/block/Makefile
    obj-$(CONFIG_MAC_FLOPPY) += swim3.o
    obj-$(CONFIG_BLK_DEV_FD) += floppy.o
    obj-$(CONFIG_AMIGA_FLOPPY) += amiflop.o
    obj-$(CONFIG_ATARI_FLOPPY) += ataflop.o
    6.3、Rules.make 部分
    第三部分只有一行:
    include $(TOPDIR)/Rules.make
    6.4、特殊规则
    第四部分含有任何必需而 Rules.make 中的通用规则无法完成的特殊的 Makefile 规则。
    7、Rules.make 变量
    Rules.make 的公共界面由以下变量组成:
    7.1、子目录
    一个 Makefile 只负责它所在目录目标文件的创建。子目录中的文件应该由那些子目录中的 Makefile 来创建。只要你让创建系统知道这些子目录,创建系统就会自动调用 make 以递归地进入子目录。
    为此,使用 subdir-{y,m,n,} 变量:
    subdir-$(CONFIG_ISDN)                   += i4l
    subdir-$(CONFIG_ISDN_CAPI)              += capi
    在实际创建内核时,例如:vmlinux (“make {vmlinux、bzImage、...”),make 将递归下降到由 $(subdir-y) 列举的目录中。
    当创建模块时 (“make modules”),make 将递归下降到由 $(subdir-m) 列举的目录中。
    当创建依赖性关系时(“make dep”),make 需要察看所有子目录,所以它将下降到由 $(subdir-y)、$(subdir-m)、$(subdir-n)、$(subdir-) 列举的所有子目录。
    您可能会遇到配置选项被设置为“y”,但您仍然需要在那个子目录中创建模块的情况。
    例如,drivers/isdn/capi/Makefile 中有:
    obj-$(CONFIG_ISDN_CAPI)                 += kernelcapi.o capiutil.o
    obj-$(CONFIG_ISDN_CAPI_CAPI20)          += capi.o
    可能 CONFIG_ISDN_CAPI=y,但 CONFIG_ISDN_CAPI_CAPI20=m。
    这可以在其父母录的 Makefile 以如下形式来表述:
    mod-subdirs                             := i4l hisax capi eicon
    subdir-$(CONFIG_ISDN_CAPI)              += capi
    即使子目录 (“capi”) 只出现在 $(subdir-y) 而不在 $(subdir-m) 中出现,使子目录 (“capi”) 出现在 $(mod-subdirs) 变量中使创建系统在 “make modules” 的过程中进入该子目录。
    7.2、目标文件目标
    O_TARGET, obj-y
    子目录 Makefile 在列表 $(obj-y) 中为 vmlinux 指定目标文件。这些列表依赖于内核配置。
    Rules.make 编译所有的 $(obj-y) 文件。而后它调用 “$(LD) -r” 以将这些文件合并成为一个名为 $(O_TARGET) 的 .o 文件。这个 $(O_TARGET) 将来由父 Makefile 连接到 vmlinux 之中。
    $(obj-y) 中文件的顺序是重要的。允许列表中出现重复:第一次出现将被连接到 $(O_TARGET),而忽略随后的出现。
    连接顺序是重要的,这是因为某些函数在启动时将按照它们出现的顺序被调用。所以要记住改变连接顺序,比如说,就可能改变您探测到 SCSI 控制器的顺序,从而改变您磁盘的编号。
    例如:
         # Makefile for the kernel ISDN subsystem and device drivers.
         # The target object and module list name.
         O_TARGET        := vmlinux-obj.o
         # Each configuration option enables a list of files.
         obj-$(CONFIG_ISDN)                      += isdn.o
         obj-$(CONFIG_ISDN_PPP_BSDCOMP)          += isdn_bsdcomp.o
         # The global Rules.make.
         include $(TOPDIR)/Rules.make

    7.3、库文件目标
    L_TARGET
    除了创建 O_TARGET 目标文件,您还可以再为 $(obj-y) 列举的目标文件创建静态连接库。通常不必这样做,它只用于 lib、arch/$(ARCH)/lib 目录。  
    7.4、可载入模块目标
    obj-m
    $(obj-m) 指定了作为可载入内核模块创建的目标文件。
    一个模块可以从一个或多个源代码文件中创建出来。如果是一个源代码文件,子目录 Makefile 仅仅将文件添加到 $(obj-m) 即可。
    例如:
         obj-$(CONFIG_ISDN_PPP_BSDCOMP)          += isdn_bsdcomp.o
    如果内核模块是从多个源代码文件中创建出来,您就以跟上面相同的方法指定您要创建的模块。
    然而,创建系统当然需要知道您要创建的模块的各个部分,所以您必须通过设定变量 $(-obj) 来告诉它。
    例如:
         obj-$(CONFIG_ISDN)                      += isdn.o
         isdn-objs := isdn_net.o isdn_tty.o isdn_v110.o isdn_common.o
    在这个例子中,模块名为 isdn.o。Rules.make 将编译列举在 $(isdn-objs) 中的文件,而后对这些文件运行“$(LD) -r”以生成 isdn.o。
    注意:当然,当您将目标文件创建到内核之中时,以上语法仍然是有效的。所以,如果您的 CONFIG_ISDN=y,创建系统将按照您所预期的,由它的各个部分为您创建出 isdn.o,然后将它连接到 $(O_TARGET) 之中。  
    7.5、带有导出符号的目标文件
    export-objs
    当使用可载入模块时,不是所有内核/其它模块中的全局符号 都自动成为可用的,您的模块只能使用那些显式导出的符号。
    为了让模块能够使用某个符号,要“导出”它,在源代码中使用 EXPORT_SYMBOL()。此外,您还要在 Makefile 变量 $(export-objs) 中列举所有导出符号的文件(例如,含有 EXPORT_SYMBOL() 指令的源代码)。
    例如:
         # Objects that export symbols.
         export-objs     := isdn_common.o
    这是因为 isdn_common.c 含有
         EXPORT_SYMBOL(register_isdn);
    这使得底层 ISDN 驱动程序能够使用函数 register_isdn。  
    7.6、编译标志
    EXTRA_CFLAGS、EXTRA_AFLAGS、EXTRA_LDFLAGS、EXTRA_ARFLAGS
    $(EXTRA_CFLAGS) 指定用 $(C) 编译 C 文件的选项。该变量中的选项用于当前目录中编译所有文件用的 $(CC) 命令。
    例如:
      # drivers/sound/emu10k1/Makefile
      EXTRA_CFLAGS += -I.
      ifdef DEBUG
          EXTRA_CFLAGS += -DEMU10K1_DEBUG
      endif
    当前目录的子目录并不使用 $(EXTRA_CFLAGS)。此外,它也不用于由 $(HOSTCC) 编译的文件。
    由于顶层 Makefile 拥有变量 $(CFLAGS) 并将该变量用于整个源代码树,因此 $(EXTRA_CFLAGS) 是必需的。
    $(EXTRA_AFLAGS) 是用于编译汇编语言源代码的类似的单个目录选项字符串。
    示例:在编写本文时,内核源代码中尚无使用 $(EXTRA_AFLAGS) 的例子。
    $(EXTRA_LDFLAGS) 和 $(EXTRA_ARFLAGS) 用于 $(LD) 和 $(AR) 的类似的单个目录选项字符串。
    示例:在编写本文时,内核源代码中尚无使用 $(EXTRA_LDFLAGS) 或 $(EXTRA_ARFLAGS) 的例子。
    CFLAGS_$@, AFLAGS_$@
    $(CFLAGS_$@) 为单个文件指定 $(CC) 选项。$@ 部分的值是要指定的文件名的字符串。
    例如:
      # drivers/scsi/Makefile
      CFLAGS_aha152x.o =   -DAHA152X_STAT -DAUTOCONF
      CFLAGS_gdth.o    = # -DDEBUG_GDTH=2 -D__SERIAL__ -D__COM2__
             -DGDTH_STATISTICS
      CFLAGS_seagate.o =   -DARBITRATE -DPARITY -DSEAGATE_USE_ASM
    这三行分别为 aha152x.o、gdth.o 和 seagate.o 指定编译选项。
    $(AFLAGS_$@) 给出类似的用于编译汇编语言源文件的选项。
    例如:
      # arch/arm/kernel/Makefile
      AFLAGS_head-armv.o := -DTEXTADDR=$(TEXTADDR) -traditional
      AFLAGS_head-armo.o := -DTEXTADDR=$(TEXTADDR) -traditional
    Rules.make
    有让目标文件依赖于用于编译它的 $(CFLAGS_$@) 的值的功能。因此,如果你为某个文件改变了 $(CFLAGS_$@) 的值,不论是编辑
    Makefile 还是以其他某种方式覆盖了原来的值,Rules.make 都会正确地工作并以新选项重新编译您的源代码。
    请注意:由于 Rules.make 的缺陷,汇编语言没有标志依赖性。如果您为某个文件编辑了 $(AFLAGS_$@),您就必须删除目标文件以便从源文件重新创建它。
    LD_RFLAG
    使用但从未定义该变量。这似乎是某种已放弃的尝试的遗迹。  
    7.7、其它变量
    IGNORE_FLAGS_OBJS
    $(IGNORE_FLAGS_OBJS) 是一个无法自动跟踪标志依赖性关系的目标文件的列表。这是个补充功能,用于处理实现依赖性标志中的问题。(问题是标志依赖性假定 %.o 文件是从一个匹配的 %.S 或 %.c 中创建出来的。有时并不是这样)。
    USE_STANDARD_AS_RULE
    这是一个过渡变量。如果定义了 $(USE_STANDARD_AS_RULE) , Rules.make 就为将 %.S 文件汇编为 %.o 文件或 %.s 文件 (%.s 文件只对开发者才有用) 提供标准规则。
    如果没有定义 $(USE_STANDARD_AS_RULE),那么 Rules.make 就不提供这些标准规则。在这种情况下,子目录 Makefile 必须为汇编 %.S 文件提供它的私有规则。

    过去,所有 Makefile 都提供私有的 %.S 规则。较新的 Makefiles 应该定义 USE_STANDARD_AS_RULE
    并使用标准的 Rules.make 规则。一旦所有体系结构的所有 Makefile 都使用 USE_STANDARD_AS_RULE,那么
    Rules.make 就可以取消对 USE_STANDARD_AS_RULE 的测试。此后,所有其它 Makefile 就可以取消对
    USE_STANDARD_AS_RULE 的定义。
    8、新风格变量
    [ 本章回溯到将前面描述的编写 Makefile 的方式称为“新风格”的时候。由于换句话说它仍然表述了同样的事,所以我在此保持了这种说法,这可能有些用处 ]
    “新风格变量”比“旧风格变量”更简单更有效。因此,许多子目录 Makefile 可以缩减 60%。作者希望所有体系结构 Makefile 和子目录 Makefile 都能及时转换到新风格。
    Rules.make 并不理解新风格变量。因此,每个新风格 Makefile 都有一个样板代码部分以便将新风格变量转换为旧风格变量。这有点混淆,人们以“新风格”定义大部分变量但最后几行又落入了“旧风格”。
    8.1、新变量
    obj-y obj-m obj-n obj-
    这些变量代替了 $(O_OBJS)、$(OX_OBJS)、$(M_OBJS) 和 $(MX_OBJS)。
    例如:
      # drivers/block/Makefile
      obj-$(CONFIG_MAC_FLOPPY)        += swim3.o
      obj-$(CONFIG_BLK_DEV_FD)        += floppy.o
      obj-$(CONFIG_AMIGA_FLOPPY)      += amiflop.o
      obj-$(CONFIG_ATARI_FLOPPY)      += ataflop.o
    请注意这里用 $(CONFIG_...) 在赋值操作符左侧进行替换。这使 GNU Make 具有组合索引的能力!每个这样的赋值替换了旧风格 Makefile 中的八行代码。
    在执行所有的赋值之后,子目录 Makefile 就创建了四个列表:$(obj-y)、$(obj-m)、 $(obj-n) 和 $(obj-)。
    $(obj-y) 是包含在 vmlinux 之中的文件的列表。
    $(obj-m) 是作为单独模块创建的文件列表。
    忽略 $(obj-n) 和 $(obj-)。
    每个列表都可能含有重复项目;此后重复的项目将被自动删除。同时出现在 $(obj-y) 和 $(obj-m) 中的文件将自动从 $(obj-m) 列表中删除。
    Example:
      # drivers/net/Makefile
      ...
      obj-$(CONFIG_OAKNET) += oaknet.o 8390.o
      ...
      obj-$(CONFIG_NE2K_PCI) += ne2k-pci.o 8390.o
      ...
      obj-$(CONFIG_STNIC) += stnic.o 8390.o
      ...
      obj-$(CONFIG_MAC8390) += daynaport.o 8390.o
      ...

    这个例子中,四个不同的驱动程序都需要 8390.o 中的代码。如果这四个驱动程序中的一个或多个要创建到 vmlinux
    之中,即使其它驱动程序需要 8390.o 作为模块进行创建,8390.o 就会被创建到 vmlinux
    之中而不是成为一个模块。(模块化的驱动程序能够使用常驻于 vmlinux 映像中的 8390.o 代码提供的服务)。
    export-objs
    $(export-objs) 是子目录中所有可能导出符号的文件的列表。构造该列表的规范方法是:
         grep -l EXPORT_SYMBOL *.c
    (但看看那些在引入头文件中偷偷调用 EXPORT_SYMBOL 的文件吧!)
    这是个可能的列表,与内核配置无关。$(export-objs) 列举出所有导出符号的文件。而后样板代码就用 $(export-objs) 来将实际的文件列表划分为 $(*_OBJS) 和 $(*X_OBJS)。
    经验表明在旧风格 Makefile 中维护一个正确的 X 变量有困难并容易出错。在新风格 Makefile 中维护 $(export-objs) 比较简单并便于核对。
    $(foo)-objs
    某些内核模块由多个目标文件连接组成。
    对于每个多部分内核模块,都有一个列表列出所有组成该模块的目标文件。对于名为 foo.o 的内核模块,它的目标文件列表就是 foo-objs。
    例如:
      # drivers/scsi/Makefile
      list-multi := scsi_mod.o sr_mod.o initio.o a100u2w.o
      ...
      scsi_mod-objs := hosts.o scsi.o scsi_ioctl.o constants.o
           scsicam.o scsi_proc.o scsi_error.o
           scsi_obsolete.o scsi_queue.o scsi_lib.o
           scsi_merge.o scsi_dma.o scsi_scan.o
           scsi_syms.o
      sr_mod-objs := sr.o sr_ioctl.o sr_vendor.o
      initio-objs := ini9100u.o i91uscsi.o
      a100u2w-objs := inia100.o i60uscsi.o
    子目录 Makefile 将以常见的配置相关方式将模块加入 obj-* 列表:
      obj-$(CONFIG_SCSI)  += scsi_mod.o
      obj-$(CONFIG_BLK_DEV_SR) += sr_mod.o
      obj-$(CONFIG_SCSI_INITIO) += initio.o
      obj-$(CONFIG_SCSI_INIA100) += a100u2w.o
    假定 CONFIG_SCSI=y。那么 vmlinux 就需要把它的 14 组件连接为 scsi_mod.o。
    假定 CONFIG_BLK_DEV_SR=m。那么 sr_mod.o 的三个组件就要以 “$(LD) -r” 连接到一起以组成内核模块 sr_mod.o。
    再假定 CONFIG_SCSI_INITIO=n。那么 initio.o 就进入 $(obj-n) 列表而且这就是它的终点了。不会编译它的组件文件,也不会创建组合文件。
    subdir-y subdir-m subdir-n subdir-
    这些变量替代了 $(ALL_SUB_DIRS)、$(SUB_DIRS) 和 $(MOD_SUB_DIRS)。
    例如:
      # drivers/Makefile
      subdir-$(CONFIG_PCI)  += pci
      subdir-$(CONFIG_PCMCIA)  += pcmcia
      subdir-$(CONFIG_MTD)  += mtd
      subdir-$(CONFIG_SBUS)  += sbus
    这些变量按类似于 obj-* 的方式工作,但用于子目录而不是目标文件。
    在所有赋值完成后,子目录 Makefile 就组成了四个列表:$(subdir-y)、$(subdir-m)、 $(subdir-n) 和 $(subdir-)。
    $(subdir-y) 是创建 vmlinux 时应该进入的子目录列表。
    $(subdir-m) 是创建模块时应该进入的子目录列表。
    $(subdir-n) 和 $(subdir-) 只用于收集本目录的所有子目录的列表。
    除 subdir-y 以外每个列表都可能含有重复项目;此后将自动删除重复项目。
    mod-subdirs
    $(mod-subdirs) 是一个所有即使出现在 $(subdir-y) 中也应该加入 $(subdir-m) 的子目录的列表。
    例如:
      # fs/Makefile
      mod-subdirs := nls
    这意味着如果 CONFIG_NLS=y,nls 应该加入 $(subdir-y) 和 $(subdir-m)。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期二, 五月 16, 2006
          
           -->
    基本数据类型
    基本类型包括字节型(char)、整型(int)和浮点型(float/double)。
    定义基本类型变量时,可以使用符号属性signed、unsigned(对于char、int),和长度属性short、long(对
    于int、double)对变量的取值区间和精度进行说明。
    下面列举了基本类型所占位数和取值范围:
    符号属性     长度属性     基本型     所占位数     取值范围       输入符举例      输出符举例
    --            --          char         8         -2^7 ~ 2^7-1        %c          %c、%d、%u
    signed        --          char         8         -2^7 ~ 2^7-1        %c          %c、%d、%u
    unsigned      --          char         8         0 ~ 2^8-1           %c          %c、%d、%u
    [signed]      short       [int]        16        -2^15 ~ 2^15-1              %hd
    unsigned      short       [int]        16        0 ~ 2^16-1             %hu、%ho、%hx
    [signed]      --           int         32        -2^31 ~ 2^31-1              %d
    unsigned      --          [int]        32        0 ~ 2^32-1              %u、%o、%x
    [signed]      long        [int]        32        -2^31 ~ 2^31-1              %ld
    unsigned      long        [int]        32        0 ~ 2^32-1             %lu、%lo、%lx
    [signed]      long long   [int]        64        -2^63 ~ 2^63-1             %I64d
    unsigned      long long   [int]        64        0 ~ 2^64-1          %I64u、%I64o、%I64x
    --            --          float        32       +/- 3.40282e+038         %f、%e、%g
    --            --          double       64       +/- 1.79769e+308  %lf、%le、%lg   %f、%e、%g
    --            long        double       96       +/- 1.79769e+308        %Lf、%Le、%Lg
    几点说明:
    1. 注意! 表中的每一行,代表一种基本类型。“[]”代表可省略。
       例如:char、signed char、unsigned char是三种互不相同的类型;
       int、short、long也是三种互不相同的类型。
       可以使用C++的函数重载特性进行验证,如:
       void Func(char ch) {}
       void Func(signed char ch) {}
       void Func(unsigned char ch) {}
       是三个不同的函数。
    2. char/signed char/unsigned char型数据长度为1字节;
       char为有符号型,但与signed char是不同的类型。
       注意! 并不是所有编译器都这样处理,char型数据长度不一定为1字节,char也不一定为有符号型。
    3. 将char/signed char转换为int时,会对最高符号位1进行扩展,从而造成运算问题。
       所以,如果要处理的数据中存在字节值大于127的情况,使用unsigned char较为妥当。
       程序中若涉及位运算,也应该使用unsigned型变量。
    4. char/signed char/unsigned char输出时,使用格式符%c(按字符方式);
       或使用%d、%u、%x/%X、%o,按整数方式输出;
       输入时,应使用%c,若使用整数方式,Dev-C++会给出警告,不建议这样使用。
    5. int的长度,是16位还是32位,与编译器字长有关。
       16位编译器(如TC使用的编译器)下,int为16位;32位编译器(如VC使用的编译器cl.exe)下,int为32
    位。
    6. 整型数据可以使用%d(有符号10进制)、%o(无符号8进制)或%x/%X(无符号16进制)方式输入输出。
       而格式符%u,表示unsigned,即无符号10进制方式。
    7. 整型前缀h表示short,l表示long。
       输入输出short/unsigned short时,不建议直接使用int的格式符%d/%u等,要加前缀h。
       这个习惯性错误,来源于TC。TC下,int的长度和默认符号属性,都与short一致,
       于是就把这两种类型当成是相同的,都用int方式进行输入输出。
    8. 关于long long类型的输入输出:
       "%lld"和"%llu"是linux下gcc/g++用于long long int类型(64 bits)输入输出的格式符。
       而"%I64d"和"%I64u"则是Microsoft VC++库里用于输入输出__int64类型的格式说明。
       Dev-C++使用的编译器是Mingw32,Mingw32是x86-win32 gcc子项目之一,编译器核心还是linux下的gcc。
       进行函数参数类型检查的是在编译阶段,gcc编译器对格式字符串进行检查,显然它不认得"%I64d",
       所以将给出警告“unknown conversion type character `I' in format”。对于"%lld"和"%llu",gcc理
    所当然地接受了。
       Mingw32在编译期间使用gcc的规则检查语法,在连接和运行时使用的却是Microsoft库。
       这个库里的printf和scanf函数当然不认识linux gcc下"%lld"和"%llu",但对"%I64d"和"%I64u",它则是
    乐意接受,并能正常工作的。
    9. 浮点型数据输入时可使用%f、%e/%E或%g/%G,scanf会根据输入数据形式,自动处理。
       输出时可使用%f(普通方式)、%e/%E(指数方式)或%g/%G(自动选择)。
    10. 浮点参数压栈的规则:float(4 字节)类型扩展成double(8 字节)入栈。
        所以在输入时,需要区分float(%f)与double(%lf),而在输出时,用%f即可。
        printf函数将按照double型的规则对压入堆栈的float(已扩展成double)和double型数据进行输出。
        如果在输出时指定%lf格式符,gcc/mingw32编译器将给出一个警告。
    11. Dev-C++(gcc/mingw32)可以选择float的长度,是否与double一致。
    12. 前缀L表示long(double)。
        虽然long double比double长4个字节,但是表示的数值范围却是一样的。
        long double类型的长度、精度及表示范围与所使用的编译器、操作系统等有关。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
             
           -->
    让GCC编译关键字“__attribute__”给你带来方便
    直接引入我们的主角(粗体部分):   int  my_printf (void *my_object, const char *my_format, ...)                __attribute__ ((format (printf, 2, 3)));my_printf是一个你自己写的函数,比如可能是对vsnprintf等函数进行了封装等等。粗体部分关键字“__attribute__”可以为函数声明赋属性值,其目的是让编译程序可以优化处理。 关键字“__attribute__”可以为函数(Function Attributes),变量(Variable Attributes)和结构成员(Type Attributes)赋属性。具体可以查看gcc手册(http://gcc.gnu.org/onlinedocs/)。这里用到的是函数属性,其语法为:“__attribute__ ((attribute-list))”,并置于函数声明尾部“;”之前。 format (archetype, string-index, first-to-check) format属性告诉编译器,按照printf, scanf, strftime或strfmon的参数表格式规则对该函数的参数进行检查。“archetype”指定是哪种风格;“string-index”指定传入函数的第几个参数是格式化字符串;“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。 例如:上述例子中,函数my_printf对其参数按“printf”的参数格式进行检查,my_printf的第二个参数的格式化字符串,从my_printf的第三个参数开始进行检查。 还有,在编译时只用指定了-Wformat选项,才会出现警告信息。(我们一般都是-Wall的,没问题) 这样,利用这样一个GCC的编译关键字就能在编译时对你指定的函数进行某种规则检查。赶快行动,在你的程序中加入这些,让编译器来为你做检查吧。(我在程序中加入这些,马上编译,立刻就发现一个long型变量使用%d进行格式化的错误) 引出的一个题外话:如果上述函数my_printf是一个类的成员函数,那么上述声明就应该写成:   int  my_printf (void *my_object, const char *my_format, ...)                __attribute__ ((format (printf, 3, 4)));为什么现在是3,4? 因为类成员函数有一个隐含的参数“this”指针作为函数的第一个参数。
    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期六, 四月 29, 2006
          
           -->
    linux内核进程调度以及定时器实现机制
    摘自
    http://blog.csdn.net/joshua_yu/archive/2006/02/02/591038.aspx
    一、2.6版以前内核进程调度机制简介
    Linux的进程管理由进程控制块、进程调度、中断处理、任务队列、定时器、bottom half队列、系统调用、进程通信等等部分组成。
    进程调用分为实时进程调度和非实时进程调度两种。前者调度时,可以采用基于动态优先级的轮转法(RR),也可以采用先进现出算法(FIFO)。后者调度时,一律采用基于动态优先级的轮转法。某个进程采用何种调度算法由改进程的进程控制块中的某些属性决定,没有专门的系统用来处理关于进程调度的相关事宜。Linux的进程调度由schedule()函数负责,任何进程,当它从系统调用返回时,都会转入schedule(),而中断处理函数完成它们的响应任务以后,也会进入schedule()。

    1.         进程控制块数据结构
    Linux系统的进程控制块用数据结构task_struct表示,这个数据结构占用1680个字节,具体的内容不在这里介绍,详细内容见《Linux内核2.4版源代码分析大全》第二页。
    进程的状态主要包括如下几个:
    TASK_RUNNING   正在运行或在就绪队列run-queue中准备运行的进程,实际参与进程调度。
    TASK_INTERRUPTIBLE       处于等待队列中的进程,待资源有效时唤醒,也可由其它进程通过信号或定时中断唤醒后进入就绪队列run-queue。
    TASK_UNINTERRUPTIBLE         处于等待队列的进程,待资源有效时唤醒,不也可由其它进程通过信号或者定时中断唤醒。
    TASK_ZOMBIE      表示进程结束但尚未消亡的一种状态(僵死),此时,进程已经结束运行并且已经释放了大部分资源,但是尚未释放进程控制块。
    TASK_STOPPED    进程暂停,通过其它进程的信号才能唤醒。

    所有进程(以PCB形式)组成一个双向列表。next_task和prev_task就是链表的前后向指针。链表的头尾都是init_task(init进程)。不过进程还要根据其进程ID号插入到一个hash表当中,目的是加快进程搜索速度。

    2.         进程调度
    Linux进程调度由schedule()执行,其任务是在run-queue队列中选出一个就绪进程。
    每个进程都有一个调度策略,在它的task_struct中规定(policy属性),或为SCHED_RR,SCHED_FIFO,或为SCHED_OTHER。前两种为实时进程调度策略,后一种为普通进程调度策略。
    用户进程由do_fork()函数创建,它也是fork系统调用的执行者。do_fork()创建一个新的进程,继承父进程的现有资源,初始化进程时钟、信号、时间等数据。完成子进程的初始化后,父进程将它挂到就绪队列,返回子进程的pid。
    进程创建时的状态为TASK_UNINTERRUPTIBLE,在do_fork()结束前被父进程唤醒后,变为TASK_RUNNING。处于TASK_RUNNING状态的进程被移到就绪队列中,当适当的时候由schedule()按CPU调度算法选中,获得CPU。
    如果进程采用轮转法,当时间片到时(10ms的整数倍),由时钟中断触发timer_interrupt()函数引起新一轮的调度,把当前进程挂到就绪队列的尾部。获得CPU而正在运行的进程若申请不到某个资源,则调用sleep_on()或interruptible_sleep_on()睡眠,并进入就绪队列尾。状态尾TASK_INTERRUPTIBLE的睡眠进程当它申请的资源有效时被唤醒,也可以由信号或者定时中断唤醒,唤醒以后进程状态变为TASK_RUNNING,并进入就绪队列。
    首先介绍一下2.6版以前的的调度算法的主要思想,下面的schedule()函数是内核2.4.23中摘录的:
    asmlinkage void schedule(void)
    {
    struct schedule_data * sched_data;
    struct task_struct *prev, *next, *p;
    struct list_head *tmp;
    int this_cpu, c;

    spin_lock_prefetch(&runqueue_lock);

    BUG_ON(!current->active_mm);
    need_resched_back:
           /*记录当前进程和处理此进程的CPU号*/
    prev = current;
    this_cpu = prev->processor;
    /*判断是否处在中断当中,这里不允许在中断处理当中调用sechedule()*/
    if (unlikely(in_interrupt())) {
            printk("Scheduling in interruptn");
            BUG();
    }

    release_kernel_lock(prev, this_cpu);

    /*'sched_data' 是收到保护的,每个CPU只能运行一个进程。*/
    sched_data = & aligned_data[this_cpu].schedule_data;

    spin_lock_irq(&runqueue_lock);

    /*如果当前进程的调度策略是轮转RR,那么需要判断当前进程的时间片是否已经用完,如果已经用完,则重新计算时间片值,然后将该进程挂接到就绪队列run-queue的最后*/
    if (unlikely(prev->policy == SCHED_RR))
            if (!prev->counter) {
                   prev->counter = NICE_TO_TICKS(prev->nice);
                   move_last_runqueue(prev);
            }
    /*假如前进程为TASK_INTERRUPTTIBLE状态,则将其状态置为TASK_RUNNING。如是其它状态,则将该进程转为睡眠状态,从运行队列中删除。(已不具备运行的条件) */
    switch (prev->state) {
            case TASK_INTERRUPTIBLE:
                   if (signal_pending(prev)) {
                          prev->state = TASK_RUNNING;
                          break;
                   }
            default:
                   del_from_runqueue(prev);
            case TASK_RUNNING:;
    }
    /*当前进程不需要重新调度*/
    prev->need_resched = 0;

    /*下面是一般的进程调度过程*/

    repeat_schedule:
    next = idle_task(this_cpu);
    c = -1000;
    /*遍历进程就绪队列,如果该进程能够进行调度(对于SMP来说就是判断当前CPU未被占用能够执行这个进程,对于非SMP系统则为1),则计算该进程的优先级,如果优先级大于当前进程,则next指针指向新的进程,循环直到找到优先级最大的那个进程*/
    list_for_each(tmp, &runqueue_head) {
            p = list_entry(tmp, struct task_struct, run_list);
            if (can_schedule(p, this_cpu)) {
                   int weight = goodness(p, this_cpu, prev->active_mm);
                   if (weight > c)
                          c = weight, next = p;
            }
    }

    /* 判断是否需要重新计算每个进程的时间片,判断的依据是所有正准备进行调度的进程时间片耗尽,这时,就需要对就绪队列中的每一个进程都重新计算时间片,然后返回前面的调度过程,重新在就绪队列当中查找优先级最高的进程执行调度。 */
    if (unlikely(!c)) {
            struct task_struct *p;

            spin_unlock_irq(&runqueue_lock);
            read_lock(&tasklist_lock);
            for_each_task(p)
                   p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
            read_unlock(&tasklist_lock);
            spin_lock_irq(&runqueue_lock);
            goto repeat_schedule;
    }

    /*CPU私有调度数据中记录当前进程的指针,并且将当前进程与CPU绑定,如果待调度进程与前面一个进程属于同一个进程,则不需要调度,直接返回。*/
    sched_data->curr = next;
    task_set_cpu(next, this_cpu);
    spin_unlock_irq(&runqueue_lock);

    if (unlikely(prev == next)) {
            /* We won't go through the normal tail, so do this by hand */
            prev->policy &= ~SCHED_YIELD;
            goto same_process;
    }
    /*全局统计进程上下文切换次数*/
    kstat.context_swtch++;
    /*如果后进程的mm为0 (未分配页),则检查是否被在被激活的页里(active_mm),否则换页。令后进程记录前进程激活页的信息,将前进程的active_mm中的mm_count值加一。将cpu_tlbstate[cpu].state改为 TLBSTATE_LAZY(采用lazy模式) 如果后进程的mm不为0(已分配页),但尚未激活,换页。切换mm(switch_mm)。 如果前进程的mm 为0(已失效) ,将其激活记录置空,将mm结构引用数减一,删除该页。 */
    prepare_to_switch();
    {
            struct mm_struct *mm = next->mm;
            struct mm_struct *oldmm = prev->active_mm;
            if (!mm) {
                   BUG_ON(next->active_mm);
                   next->active_mm = oldmm;
                   atomic_inc(&oldmm->mm_count);
                   enter_lazy_tlb(oldmm, next, this_cpu);
            } else {
                   BUG_ON(next->active_mm != mm);
                   switch_mm(oldmm, mm, next, this_cpu);
            }

            if (!prev->mm) {
                   prev->active_mm = NULL;
                   mmdrop(oldmm);
            }
    }

    /*切换到后进程,调度过程结束*/
    switch_to(prev, next, prev);
    __schedule_tail(prev);

    same_process:
    reacquire_kernel_lock(current);
    if (current->need_resched)
            goto need_resched_back;
    return;
    }

    3.         进程上下文切换(摘自中国Linux论坛一片文章)
    首先进程切换需要做什么?它做的事只是保留正在运行进程的"环境",并把将要运行的进程的"环境"加载上来,这个环境也叫上下文。它包括各个进程"公用"的东西,比如寄存器。 下一个问题,旧的进程环境保存在那,新的进程环境从那来,在i386上,有个tss段,是专用来保存进程运行环境的。在Linux来说,在结构task_struct中有个类型为struct thread_struct的成员叫tss,如下:
    struct task_struct {
    。。。
    /* tss for this task */
    struct thread_struct tss;
    。。。
    };
    它是专用来存放进程环境的,这个结构体因CPU而异,你看它就能知道有那些寄存器是需要保存的了。
    最后的问题就是切换了,虽然在i386上CPU可以自动根据tss去进行上下文的切换,但是Linux的程序员们更愿意自己做它,原因是这样能得到更有效的控制,而且作者说这和硬件切换的速度差不多,这可是真的够伟大的。
    好了,现在来看源码,进程切换是使用switch_to这个宏来做的,当进入时prev即是现在运行的进程,next是接下来要切换到的进程,
    #define switch_to(prev,next,last) do {  
    asm volatile(
    "pushl %%esint"  
    "pushl %%edint"  
    "pushl %%ebpnt"  

    // 首先它切换堆栈指针,prev->tss.esp = %esp;%esp = next->tss.esp,这以后的堆栈已经是next的堆栈了。
    "movl %%esp,%0nt" /* save ESP */  
    "movl %3,%%espnt" /* restore ESP */  

    // 然后使进程prev的指针保存为标号为1的那一个指针,这样下次进程prev可以运行时,它第一个执行的就是pop指令。
    "movl $1f,%1nt" /* save EIP */  

    // 把进程next保存的指针推进堆栈中,这句作用是,从__switch_to返回时,下一个要执行的指令将会是这个指针所指向的指令了。
    "pushl %4nt" /* restore EIP */  

    // 使用jump跳到__switch_to函数的结果是:调用switch_to函数但不象call那样要压栈,但是ret返回时,仍是要弹出堆栈的,也就是上条指令中推进去的指令指针。这样,堆栈和指令都换了,进程也就被"切换"了。
    "jmp __switch_ton"  

    // 由于上面所说的原因,__switch_to返回后并不会执行下面的语句,要执行到这,只有等进程prev重新被调度了。
    "1:t"  
    "popl %%ebpnt"  
    "popl %%edint"  
    "popl %%esint"  
    :"=m" (prev->tss.esp),"=m" (prev->tss.eip),  
    "=b" (last)  
    :"m" (next->tss.esp),"m" (next->tss.eip),  
    "a" (prev), "d" (next),  
    "b" (prev));  
    } while (0)

    最后是__switch_to函数,它虽然是c形式,但内容还都是嵌入汇编。

    // 这句跟fpu有关,我并不感兴趣,就略过了。
    unlazy_fpu(prev);

    // 这句可能需要你去记起前面第二章中所描述的gdt表的结构了,它的作用是把进程next的tss描述符的type中的第二位清0,这位表示这个描述符是不是当前正在用的描述符,作者说如果不清0就把它load进tss段寄存器的话,系统会报异常(我可没试过)。
    gdt_table[next->tss.tr >> 3].b &= 0xfffffdff;

    // 把进程next的tss段load到tss段存器中。
    asm volatile("ltr %0": :"g" (*(unsigned short *)&next->tss.tr));

    // 保存进程prev的fs和gs段寄存器
    asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->tss.fs));
    asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->tss.gs));

    然后下面就是load进程next的ldt,页表,fs,gs,debug寄存器。
    因为Linux一般并不使用ldt,所以它们一般会指向一个共同的空的ldt段描述符,这样就可能不需要切换ldt了,如果进程next和prev是共享内存的话,那么页表的转换也就不必要了(这一般发生在clone时)。

    二、2.6版内核对进程调度的优化
    1.         新调度算法简介
    2.6版本的Linux内核使用了新的调度器算法,称为O(1)算法,它在高负载的情况下执行得非常出色,并在有多个处理器时能够很好地扩展。
    2.4版本的调度器中,时间片重算算法要求在所有的进程都用尽它们的时间片后,新时间片才会被重新计算。在一个多处理器系统中,当进程用完它们的时间片后不得不等待重算,以得到新的时间片,从而导致大部分处理器处于空闲状态,影响SMP的效率。此外,当空闲处理器开始执行那些时间片尚未用尽的、处于等待状态的进程时,会导致进程开始在处理器之间“跳跃”。当一个高优先级进程或交互式进程发生跳跃时,整个系统的性能就会受到影响。
    新调度器解决上述问题的方法是,基于每个CPU来分布时间片,并取消全局同步和重算循环。调度器使用了两个优先级数组,即活动数组和过期数组,可以通过指针来访问它们。活动数组中包含所有映射到某个CPU且时间片尚未用尽的任务。过期数组中包含时间片已经用尽的所有任务的有序列表。如果所有活动任务的时间片都已用尽,那么指向这两个数组的指针互换,包含准备运行任务的过期数组成为活动数组,而空的活动数组成为包含过期任务的新数组。数组的索引存储在一个64位的位图中,所以很容易找到最高优先级的任务。

    新调度器的主要优点包括:
    ◆     SMP效率 如果有工作需要完成,所有处理器都会工作。
    ◆     等待进程 没有进程需要长时间地等待处理器,也没有进程会无端地占用大量的CPU时间。

    作者

    teren
    (   
    技术
      )
      
    ::
    最新回复
    (0) ::
       静态链接网址 ::
       引用 (0)
       
               星期三, 四月 26, 2006
          
           -->
    嵌入式软件设计中查找缺陷的几个技巧
    摘自
    http://www.uml.org.cn/embeded/200603105.htm

    部分软件开发项目依靠结合代码检查、结构测试和功能测试来识别软件缺陷。尽管这些传统技术非常重要,而且能发现大多数软件问题,但它们无法检查出当今复杂
    系统中的许多共性错误。本文将介绍如何避免那些隐蔽然而常见的错误,并介绍的几个技巧帮助工程师发现软件中隐藏的错误。

    构测试或白盒测试能有效地发现代码中的逻辑、控制流、计算和数据错误。这项测试要求对软件的内部工作能够一览无遗(因此称为"白盒"或"玻璃盒"),以便
    了解软件结构的详细情况。它检查每个条件表达式、数学操作、输入和输出。由于需要测试的细节众多,结构测试每次检查一个软件单元,通常为一个函数或类。
    代码审查也使用与实现缺陷和潜在问题查找同样复杂的技术。与白盒测试一样,审查通常针对软件的各个单元进行,因为一个有效的审查过程要求的是集中而详尽的检查。

    审查和白盒测试不同,功能测试或黑盒测试假设对软件的实现一无所知,它测试由受控输入所驱动的输出。功能测试由测试人员或开发人员所编写的测试过程组成,
    它们规定了一组特定程序输入对应的预期程序输出。测试运行之后,测试人员将实际输出与预期输出进行比较,查找问题。黑盒测试可以有效地找出未能实现的需
    求、接口问题、性能问题和程序最常用功能中的错误。
    虽然将这些技术结合起来可以找出隐藏
    在一个特定软件程序中的大部分错误,但它们也有局限。代码审查和白盒测试每次只针对一小部分代码,忽视了系统的其它部分。黑盒测试通常将系统作为一个整体
    来处理,忽视了实现的细节。一些重要的问题只有在集中考察它们在整个系统内相互作用时的细节才能被发现;传统的方法无法可靠地找出这些问题。必须整体地检
    查软件系统,查找具体问题的特定原因。由于详尽彻底地分析程序中的每个细节和它与代码中所有其它部分之间的相互作用通常是不大可能的,因此分析应该针对程
    序中已经知道可能导致问题的特定方面。本文将探讨其中三个潜在的问题领域:
    * 堆栈溢出
    * 竞争条件
    * 死锁
    读者可在网上阅读本文的第二部分,它将探讨下列问题:
    * 时序问题
    * 可重入条件
    在采用多任务实时设计技术的系统中,以上所有问题都相当普遍。
    堆栈溢出

    理器使用堆栈来存储临时变量、向被调函数传递参数、保存线程“状态”,等等。如果系统不使用虚拟内存(换句话说,它不能将内存页面转移到磁盘上以释放内存
    空间供其它用途),堆栈将固定为产品出厂时的大小。如果由于某种原因堆栈越出了编程人员所分配的数量范围,程序将变得不确定。这种不稳定可能导致系统发生
    严重故障。因此,确保系统在最坏情况下能够分配到足够的堆栈至关重要。
    确保永不发生堆栈溢出的唯一途径就是分析代码,确定程序在各种可能情况下的最大堆栈用量,然后检查是否分配了足够的堆栈。测试不大可能触发特定的瞬时输入组合进而导致系统出现最坏情况。
    堆栈深度分析的概念比较简单:
    1. 为每个独立的线程建立一棵调用树。
    2. 确定调用树中每个函数的堆栈用量。
    3. 检查每棵调用树,确定从树根到外部“树叶”的哪条调用路径需要使用的堆栈最多。
    4. 将每个独立线程调用树的最大堆栈用量相加。
    5. 确定每个中断优先级内各中断服务程序(ISR)的最大堆栈用量并计算其总和。但是,如果ISR本身没有堆栈而使用被中断线程的堆栈,则应将ISR使用的最大堆栈数加到各线程堆栈之上。
    6. 对于每个优先级,加上中断发生时用来保存处理器状态的堆栈数。
    7.如果使用RTOS,则加上RTOS自身内部用途需要的最大堆栈数(与应用代码引发的系统调用不同,后者已包含在步骤2中)。

    此之外,还有两个重要事项需要考虑。首先,仅仅从高级语言源代码建立的调用树很可能并不完善。大部分编译器采用运行时库(run-time
    library)来优化常用计算任务,如大值整数的乘除、浮点运算等,这些调用只在编译器产生的汇编语言中才可见。运行时库函数本身可能使用大量的堆栈空
    间,在分析时必须将它们包括进去。如果使用的是C++语言,则以下所有类型的函数(方法)也都必须包含到调用树内:结构器、析构器、重载运算符、复制结构
    器和转换函数。所有的函数指针也都必须进行解析,并且将它们调用的函数包含进分析之中。

    二,编译器使用一个C库来实现memcpy()、cos()和atof
    ()等标准函数,而这些例程的源代码可能无法得到。如果能够得到它们的源代码,就有可能确定程序用到的每个库调用在最坏情况下的堆栈使用数量。如果这些库
    只包含在目标文件中,则编译器厂商必须提供每个库例程使用的堆栈数。如果没有这些信息,就无法通过分析来确定最坏情况下程序使用的最大堆栈数。幸运的是,
    许多面向嵌入式系统的编译器厂商都提供这些信息。
    通常,每次一个函数被调用时,编译器将
    使用堆栈来保存返回地址并传递函数参数。函数的自动(局部)变量通常也在堆栈当中。不过,由于编译器会尽可能通过将参数或局部变量放入寄存器来优化代码,
    因此检查汇编语言以精确地确定堆栈用量非常重要。编译器也有可能在代码中的其它地方选择使用堆栈,如用堆栈来保存中间计算结果。

    些与编译器一起打包销售的开发环境包含生成调用树的工具,还有许多第三方的调用树生成工具。但是,除非它们能够对汇编语言进行分析,否则这些工具可能会遗
    漏运行时库和C库的调用。不过无论在哪种情况下,开发分析汇编语言文件并提取函数名称以及各函数内部调用的脚本都比较简单。分析的结果可写入一个文件,而
    这个文件能够方便地输入到表格之中。
    确定了各个函数的堆栈用量之后,必须计算每个线程所
    需的最大堆栈数。由于一般程序通常涉及数百个函数,调用跨越多层深度,处理这些信息的一种简便方法就是采用分析表格。如表1所示,表格的各行包含了函数名
    称、该函数使用的最大堆栈数(包括调用其它函数所需的堆栈数),以及它调用的所有函数的清单。通过编程控制,这个表格从每个函数的"根"开始迭代循环,计
    算该函数及其调用的所有函数需要的堆栈。这些信息存放在堆栈路径列中,这样,采用每个线程根函数(如main)的堆栈路径数据就可以方便地计算出需要的最
    大堆栈数了。这个过程包含了先前介绍的堆栈分析过程中的前四个步骤。
    有时候,采用堆栈深度分析过程可能是无法做到,或者是不实际的。如果无法得到运行时库或C库的源代码,而编译器厂商又没有提供任何堆栈使用信息,就不可能进行完整的堆栈分析。在这种情况下,有两种选择:
    1. 在测试期间,观察堆栈所能达到的深度,并保证有较大的堆栈空间余量。
    2. 检测堆栈溢出,并采取改进措施。
    观察堆栈深度的方法很简单:
    * 向整个内存堆栈区写入一个特定的数据图案符号,如55AA。
    * 在预期使用最大堆栈空间的条件下运行系统。
    * 使用仿真器或其它工具检查堆栈存储区,看有多少符号图案由于堆栈的使用而被改写了。
    当然,这些步骤并不能保证在一些不同条件下不会需要更多的堆栈,但确实可以表明所需要的最小堆栈数。
    使
    用带内存管理单元(MMU)的处理器时,有可能检测出运行时的堆栈溢出现象。MMU将内存划分为多个区域,用一个受保护的内存段来“警戒”堆栈区域。发生
    堆栈溢出时,处理器将访问这个受保护段。这个操作将引发一个异常事件(如产生SIGSEGV信号),可被程序捕获到。创建线程时,与实时POSIX标准兼
    容的RTOS提供有这种堆栈警戒功能选项,大大简化了编程人员的工作。GNU工具等其它开发环境包含有编译器开关,可在程序中添加实现堆栈警戒功能所需的
    代码,但它们仍然依靠底层操作系统来有效地处理堆栈溢出。但是,按照这种方式检测溢出还只是问题的一部分。为了使这类设计更为有效,系统必须能够从堆栈溢
    出中恢复过来并继续正确地工作。
    在一个对安全或任务要求严格的应用中,系统运行时在测试
    或检测堆栈溢出期间监视堆栈的深度可能并不是一项足够的风险控制措施。对于一些应用,必须确保系统绝对不会越出所分配的堆栈范围;只有通过完整的堆栈深度
    分析才能证明这一点。这意味着,如果整个程序在同一内存空间运行,则必须对所有代码执行这项分析。不过,如果使用MMU,分析常可简化。在设计系统时,可
    将所有关键代码置于一个或多个独立线程内,而这些线程分别在各自的保护内存段中运行。这样,只要对这些关键线程进行堆栈使用分析就可以了。当然,这项简化
    设计假定当非关键线程溢出其堆栈并失效时,关键线程仍可正确执行。
    由于分析工作所需的堆
    栈使用数据来自汇编语言清单,因此修改代码时,相应模块的堆栈使用信息必须予以更新。如果使用不同的编译器版本,或者改变了优化设置,也必须复核整个分析
    过程。在理想情况下,编译器将提供每个函数(如果不是每个线程的话)的堆栈使用数量,因为它拥有计算需要的所有信息。例如,瑞萨公司提供有Call
    Walker,这是该公司高性能的Embedded
    Workshop开发环境的一部分。这个工具可以图形化地显示每个函数使用的调用树和堆栈,包括运行时库和C库的函数。Call
    Walker也能找出使用堆栈数量最大的路径。使用这样的工具可以实现步骤1到步骤3的自动化。

    部分软件开发项目依靠结合代码检查、结构测试和功能测试来识别软件缺陷。尽管这些传统技术非常重要,而且能发现大多数软件问题,但它们无法检查出当今复杂
    系统中的许多共性错误。本文将介绍如何避免那些隐蔽然而常见的错误,并介绍的几个技巧帮助工程师发现软件中隐藏的错误。
    竞争条件

    两个或更多独立线程同时访问同一资源时,就出现了竞争条件。竞争条件的影响多种多样,取决于具体的情况。清单1解释了一个潜在的竞争条件。函数
    Update_Sensor()通过调用get_raw()来读取传感器的原始数据。在处理过程中,该数据被乘上一个定标因子,并加上一个偏移量。处理是
    在该数据的一个临时副本上进行的,然后,该临时副本被写入共享变量。
    如果在数据写入之
    前,使用shared_sensor的另一个线程或ISR先占(preempt)了这个线程,它将得到原来的传感器读数。使用临时副本可以防止先占线程读
    取只经过部分处理的数据。不过,如果这些代码在一个数据总线不足32位的处理器上运行,就会存在竞争条件。

    一个8位或16位的处理器上,向shared_sensor的写入操作并不是一次性完成的。在8位处理器上,写入32位浮点值可能需要四条指令,在16位
    处理器上可能需要两条指令。如果在对shared_sensor进行连续写入中途Update_Sensor()被先占,则先占线程将从由一部分老数据和
    一部分新数据组成的shared_sensor读取一个数值。根据应用的具体情况,这有可能造成严重的后果。解决的办法是锁定调度程序,或在更新共享变量
    期间禁止中断。
    消除竞争条件通常很简单,但找出隐藏在代码中的竞争条件则需要仔细的分析。

    于由一个循环程序和不同ISR组成的简单系统,分析竞争条件很简单,只需检查每个ISR并识别它引用的所有共享变量。共享变量通常是这些系统中的全局数
    据,一旦这些共享变量被找出来之后,就可以检查它们在代码中的各次使用情况。每次访问都必须按需要进行保护,以避免潜在的冲突。在简单设计中,一般通过在
    关键代码段周围禁止中断来实现保护。遵守下列规则可帮助避免竞争问题:
    * 如果一个ISR对共享数据进行写入,则该ISR之外的每次可中断的读操作都必须予以保护。
    * 如果一个ISR对共享数据进行写入,则该ISR之外的任何读-修-写操作都必须予以保护。
    * 如果一个ISR读取共享数据,则对该数据的可中断写操作必须予以保护。
    * 如果一个ISR和其它代码都要检查一个硬件状态标志,以便在使用某资源之前确定其可用性,如:
    if (!resource_busy)
    {
    // Use resource
    }
    则从检查标志之时开始,到硬件设置标志表示资源不可用为止,必须采取保护措施。

    于使用了优先级不同的多个线程的更为复杂的系统,其分析也非常相似。上述规则仍然适用于ISR使用的所有数据。此外,还必须识别出每个线程使用的共享数
    据。首先从系统中优先级最高的线程开始,找出它与任何优先级较低的线程共享的所有数据,然后按照上述四条规则进行保护。对于软件使用的其它每个优先级,再
    重复这一过程。
    注意,如果系统采用了一种循环调度算法,则特定优先级内的所有线程可在任意时刻相互先占。这意味着前述四条分析规则在考虑较低优先级的线程之外,还必须考虑同一优先级的所有线程。

    线程系统通常使用某种类型的操作系统,它能够提供多种保护选择。可以使用互斥或信号量,或者锁定调度器。有时也可使用其它进程间通信(IPC)基本技术:
    通过向消息队列发送消息(而非修改共享变量)来表示数据已经改变。在许多情况下,最好由单一线程来管理共享资源,它负责处理所有的读写请求,并在内部防止
    访问冲突。
    在复杂的代码中辨认潜在的竞争条件可能是一项乏味而又耗时的工作。相应的辅助
    工具从用来识别全局数据访问的简单脚本到先进的动态分析程序如Polyspace
    Verifier。虽然比较困难,但详尽的代码分析是识别这类错误的唯一途径。测试不大可能能够建立重复触发竞争条件所需的精确时序序列。
    死锁

    共享资源的系统中,防止访问冲突极为重要,但这有可能导致另一个问题:死锁。当通过"锁定"一个资源来防止任何其它线程访问这个资源,以避免竞争条件时,
    必须对设计进行评估,确保绝对不会发生死锁。死锁测试通常没有什么效果,因为只有某种特定顺序的资源锁定才可能产生死锁,而一般的测试不大可能导致这种顺
    序。
    死锁只不过是多线程环境中一个锁定资源的问题。以下四个条件必须同时具备,才会发生死锁。防止其中任何一个条件出现都可以排除死锁的可能性:
    * 相互排除---每次只有一个线程可以使用某个锁定的资源;
    * 非先占---其它线程不能强迫另一个线程释放资源;
    * 保持并等待---线程在等待需要的其它任何资源时,保持它们已经锁定的资源;
    * 循环等待---存在一个线程循环链,其中每个线程保持链中下一个线程所需要的资源。
    图1中的资源分配图是死锁问题的一个例子。线程1首先锁定Buf资源,在保持Buf时,指向Bus,然后是Mux。如果线程1一直运行到结束,它最终将释放所有这些资源。线程2运行时,必须指向Bus、Sem,最后是Mux。线程3运行时,需要Sem和Buf。

    这个设计实例中,无法保证任何一个线程能够在另一个线程开始执行之前结束。如果一个线程不能得到需要的某个资源,它将挂起执行(阻塞),直到该资源有效为
    止。在系统运行过程中,各线程都将对资源进行锁定或解锁。由于各线程运行和指向其资源的相对时序各不相同,有可能出现由于各个线程正在等待被其它线程保持
    的资源,导致所有线程都无法运行的情况。例如,如果线程1保持Buf,线程2保持Bus,而线程3已经取得了Sem,则系统将发生死锁。因为按照从Buf
    到Bus到Sem,再回到Buf的线程分配箭头,循环等待条件得到了满足。
    潜在死锁问题识别出来之后,通常很容易进行修复。在图2中,对线程3进行了修改,使其在得到Sem之前首先设法指向Buf。这样,循环等待的条件就被打破了,系统将不会再受到死锁的影响。

    些操作系统过多地使用消息传递来进行线程间通信和同步。在这些类型的系统中,当某线程向另一个线程传递消息时,发送线程将阻塞,直到从接收线程收到响应为
    止。接收线程通常将一直阻塞到从其它某个线程接收到一个消息为止。这些结构中也会发生死锁。为了给一个基于消息的操作系统建立一张资源分配图,我们利用消
    息通道来模拟分配的资源。图3是一个例子。线程2建立了通道T2
    Ch,当它未因为等待这个通道上的一个消息而阻塞时,线程2就将"锁定"这个通道。当它阻塞并等待一个消息时,另一个线程可在这个通道上向它发送一个消
    息,并且这个消息将立即被接收到。
    现在考虑下面这个系统:线程1指向Mutex并在通道
    T2 Ch上向线程2发送消息。在线程2中的某个地方,线程2在通道T3 Ch上向线程3发送消息。线程3也在通道T4
    Ch上向线程4发送消息。在线程4中的某个地方,它也尝试指向Mutex,如果得不到,它就将阻塞。显然,各资源之间存在一条循环路径,这表明有可能发生
    死锁。例如,如果某一时刻线程1保持Mutex而线程4尝试指向它,线程4就将在Mutex上阻塞。然后当线程3尝试在通道T4
    Ch上向线程4发送一个消息时,线程3将阻塞,等待来自线程4的应答(因为线程4是由于等待Mutex而阻塞,不是为了等待这个消息)。类似地,当线程2
    尝试向线程3发送一个消息时,将被阻塞;线程1尝试向线程2发送一个消息时也将阻塞,由于它仍然保持着Mutex,所以系统将发生死锁。
    对付死锁的最容易的办法是通过设计进行避免。采用以下任何一条设计约束都可排除死锁出现的可能性:
    * 任意时刻线程锁定的资源不超过一个。 * 线程开始执行前就完全分配它所需的全部资源。 * 指向多个资源的线程必须按照一种系统范围的预设顺序来锁定(并释放)这些资源。
    如果无法通过设计来避免死锁,则应该建立资源分配图。检查资源分配图可以识别潜在的死锁。通过仔细跟踪系统中的所有线程和它们锁定的共享资源,可以维护资源分配图并周期性地进行检查,及时发现循环等待的特征。
    建立资源分配图需要识别每个受保护的共享资源,以及指向其中某一资源的所有线程。如果使用一个操作系统,可以采用下面的过程步骤:
    1. 识别所有可能阻塞的系统调用,如Mutex_Lock(),每个受保护的共享资源总是有一些与访问它有关的阻塞调用。
    2. 识别出获取共享资源的阻塞调用之后,在源代码中查找它们的各次调用情况。
    3. 对于每次调用,记录下指向资源的线程名称和该资源的名称。通常调用本身将受保护的资源作为一个参数来传递,调用在源代码中所处的位置表明了哪个线程需要该资源。通过这种方式,可以识别出所有受保护的资源以及分配资源的线程。
    4.
    建立资源分配图,并检查是否有任何资源存在循环路径。当线程和共享资源较少时,画出资源分配图比较简单。在较为复杂的系统中,最好将这些信息输入分析表
    格,并编写一个宏来检查线程和资源分配结构,以识别潜在的死锁。编写好宏之后,就可以快速地对资源分配变化进行重新评估。编写宏时,可以忽略不会导致死锁
    的资源之间的循环。在表2所示的例子中,各种资源之间有许多循环,但只有线程6和线程7之间可能存在死锁。

    一些类型的系统中,预先确定每一个共享资源并建立分配图是不实际或不可能的。此时可以增加一些额外的代码,以便在系统运行时检测出潜在的死锁。许多不同的
    算法都致力于优化这个检测过程,但本质上它们几乎都动态地建立某种资源分配图。只要有线程请求、分配或释放资源,分配图就会被修改和检测,以确定是否存在
    表明潜在死锁的循环路径。
    检测到某个死锁之后,唯一的克服方法是强迫线程释放关键的资
    源。通常,这意味着中断正保持着所需资源的线程。对于某些应用,这种方法可能是无法接受的。另一个有趣的解决方案是在运行时收集资源分配情况并进行事后分
    析处理,以确定在程序运行过程中是否有死锁情况发生。尽管这种方法并不能防止在运行时发生死锁,但它确实有助于在死锁出现后发现问题并进行修复。
    还有一些工具也可以用来帮助发现代码中的死锁。例如,Solaris程序设计员可以采用 Sun公司的LockLint工具来对代码进行统计分析。它可以发现对锁定技术的不一致用法,识别引起竞争条件和死锁的许多原因。
                   
                   
                   

    本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/15708/showart_129451.html
  • 您需要登录后才可以回帖 登录 | 注册

    本版积分规则 发表回复

      

    北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
    未成年举报专区
    中国互联网协会会员  联系我们:huangweiwei@itpub.net
    感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

    清除 Cookies - ChinaUnix - Archiver - WAP - TOP