兴海北路

---男儿仗剑自横行
<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

统计

  • 随笔 - 85
  • 文章 - 0
  • 评论 - 17
  • 引用 - 0

常用链接

留言簿(6)

随笔分类

随笔档案

收藏夹

全是知识啊

搜索

  •  

最新评论

阅读排行榜

评论排行榜

代码测试、调试与优化的小结

by falcon<zhangjinw@gmail.com>
2008-02-29

    代码写完以后往往要做测试(或验证)、调试,可能还要优化。
    关于测试(或验证),通常对应着两个英文单词verification和validation,在资料[1]中有关于这个的定义和一些深入的讨论,在资料[2]中,很多人给出了自己的看法。但是我想正如资料[2]提到的:
    “The differences between verification and validation are unimportant except to the theorist; practitioners use the term V&V to refer to all ofthe activities that are aimed at making sure the software will function as required.”
    所以,无论测试(或验证)目的都是为了让软件的功能能够达到需求。测试和验证通常会通过一些形式化(貌似可以简单地认为有数学根据的)或者非形式化的方法去验证程序的功能是否达到要求。
    而调试对应英文debug,debug叫“驱除害虫”,也许一个软件的功能达到了要求,但是可能会在测试或者是正常运行时出现异常,因此需要处理它们。
    关于优化:debug是为了保证程序的正确性,之后就需要考虑程序的执行效率,对于存储资源受限的嵌入式系统,程序的大小也可能是优化的对象。
    很多理论性的东西是在没有研究过,暂且不说吧。这里只是想把一些需要动手实践的东西先且记录和总结一下,另外很多工具在这里都有提到和罗列,包括Linux内核调试相关的方法和工具。关于更详细更深入的内容还是建议直接看后面的参考资料为妙。

    下面的所有演示在如下环境下进行:

Quote:

$ uname -a
Linux falcon 2.6.22-14-generic #1 SMP Tue Feb 12 07:42:25 UTC 2008 i686 GNU/Linux
$ echo $SHELL
/bin/bash
$ /bin/bash --version | grep bash
GNU bash, version 3.2.25(1)-release (i486-pc-linux-gnu)
$ gcc --version | grep gcc
gcc (GCC) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)
$ cat /proc/cpuinfo | grep "model name"
model name      : Intel(R) Pentium(R) 4 CPU 2.80GHz



1、代码测试

代 码测试有很多方面呢,例如运行时间、函数调用关系图、代码覆盖度、性能测试(profiling)、内存访问越界(segmentation fault)、缓冲区溢出(stack smashing合法地进行非法的内存访问?所以很危险)、内存泄露(memory leak)等。

1.1 测试程序的运行时间 time

shell提供了内置命令time用于测试程序的执行时间,默认显示结果包括三部分:实际花费时间(real time)、用户空间花费时间(user time)和内核空间花费时间(kernel time)。

Quote:

$ time pstree 2>&1 >/dev/null

real    0m0.024s
user    0m0.008s
sys     0m0.004s



time 命令给出了程序本身的运行时间。这个测试原理非常简单,就是在程序运行(通过system函数执行)前后记录了系统时间(用times函数),然后进行求 差就可以。如果程序运行时间很短,运行一次看不到效果,可以考虑采用测试纸片厚度的方法进行测试,类似把很多纸张跌倒一起来测试纸张厚度一样,我们可以让 程序运行很多次。

如果程序运行时间太长,执行效率很低,那么得考虑程序内部各个部分的执行情况,从而对代码进行可能的优化。具体可能会考虑到这两点:

  • 对于C语言程序而言,一个比较宏观的层次性的轮廓(profile)是函数调用图、函数内部的条件分支构成的语句块,然后就是具体的语句。把握好这样一个 轮廓后,就可以有针对性地去关注程序的各个部分,包括哪些函数、哪些分支、哪些语句最值得关注(执行次数越多越值得优化,术语叫hotspots)。

  • 对于Linux下的程序而言,程序运行时涉及到的代码会涵盖两个空间,即用户空间和内核空间。由于这两个空间涉及到地址空间的隔离,在测试或调试时,可能 涉及到两个空间的工具。前者绝大多数是基于gcc的特定参数和系统的ptrace调用,而后者往往实现为内核的补丁,它们在原理上可能类似,但实际操作时 后者显然会更麻烦,不过如果你不去hack内核,那么往往无须关心后者。

    1.2 函数调用关系图 calltree

    calltree可以非常简单方便地反应一个项目的函数调用关系图,虽然诸如gprof这样的工具也能做到,不过如果仅仅要得到函数调用图,calltree应该是更好的选择。如果要产生图形化的输出可以使用它的-dot参数也可以参考资料[12]。从这里可以下载到它,ftp://ftp.berlios.de/pub/calltree/calltree-2.3.tar.bz2
    关于calltree的实现原理,可以参考资料[13],关于它的详细用法请参考资料[14]或者它的-h参数获取帮助。

    这里是一份演示结果,
    Quote:

    $ calltree -b -np -m *.c
    main:
    |   close
    |   commitchanges
    |   |   err
    |   |   |   fprintf
    |   |   ferr
    |   |   ftruncate
    |   |   lseek
    |   |   write
    |   ferr
    |   getmemorysize
    |   modifyheaders
    |   open
    |   printf
    |   readelfheader
    |   |   err
    |   |   |   fprintf
    |   |   ferr
    |   |   read
    |   readphdrtable
    |   |   err
    |   |   |   fprintf
    |   |   ferr
    |   |   malloc
    |   |   read
    |   truncatezeros
    |   |   err
    |   |   |   fprintf
    |   |   ferr
    |   |   lseek
    |   |   read$



    这 样一份结果对于“反向工程”应该会很有帮助,它能够呈现一个程序的大体结构,对于阅读和分析源代码来说是一个非常好的选择。虽然cscope和ctags 也能够提供一个函数调用的“即时”(在编辑vim的过程中进行调用)视图(view),但是calltree却给了我们一个宏观的视图。

    不过这样一个视图只涉及到用户空间的函数,如果想进一步给出内核空间的宏观视图,那么strace和KFT就可以发挥它们的作用。关于这两个工具请参考条目[11]列出的相关资料。另外,该视图也没有给出库中的函数,如果要跟踪呢?需要ltrace工具。

    另外,我们发现,calltree仅仅给出了一个程序的函数调用视图,而没有告诉我们各个函数的执行次数等情况。如果要关注这些呢?我们有gprof。

    1.3 性能测试工具 gprof & kprof

    参考资料[3]详细介绍了这个工具的用法,这里仅挑选其中一个例子来演示。gprof是一个命令行的工具,而KDE桌面环境下的kprof则给出了图形化的输出,这里仅演示前者。

    首先来看一段代码(来自资料[3]),算Fibonacci数列的,


    Code:

    [Ctrl+A Select All]



    通过calltree看看这段代码的视图,
    Quote:

    $ calltree -b -np -m *.c
    main:
    |   fibonacci
    |   |   fibonacci ....
    |   printf



    可以看出程序主要涉及到一个fibonacci函数,这个函数递归调用自己。为了能够使用gprof,需要编译时加上-pg选项,让gcc加入相应的调试信息以便gprof能够产生函数执行情况的报告。
    Quote:

    $ gcc -pg -o fib fib.c
    $ ls
    fib  fib.c



    运行程序并查看执行时间,
    Quote:

    $  time ./fib
    fibonnaci(0) = 0
    fibonnaci(1) = 1
    fibonnaci(2) = 1
    fibonnaci(3) = 2
    ...
    fibonnaci(41) = 165580141
    fibonnaci(42) = 267914296

    real    1m25.746s
    user    1m9.952s
    sys     0m0.072s
    $ ls
    fib  fib.c  gmon.out


    上面仅仅选取了部分执行结果,程序运行了1分多钟,代码运行以后产生了一个gmon.out文件,这个文件可以用于gprof产生一个相关的性能报告。
    Quote:

    $ gprof  -b ./fib gmon.out
    Flat profile:

    Each sample counts as 0.01 seconds.
      %   cumulative   self              self     total          
     time   seconds   seconds    calls  ms/call  ms/call  name   
     96.04     14.31    14.31       43   332.80   332.80  fibonacci
      4.59     14.99     0.68                             main


                            Call graph


    granularity: each sample hit covers 2 byte(s) for 0.07% of 14.99 seconds

    index % time    self  children    called     name
                                                     <spontaneous>
    [1]    100.0    0.68   14.31                 main [1]
                   14.31    0.00      43/43          fibonacci [2]
    -----------------------------------------------
                                 2269806252             fibonacci [2]
                   14.31    0.00      43/43          main [1]
    [2]     95.4   14.31    0.00      43+2269806252 fibonacci [2]
                                 2269806252             fibonacci [2]
    -----------------------------------------------


    Index by function name

       [2] fibonacci               [1] main


    从 这份结果中可观察到程序中每个函数的执行次数等情况,从而找出值得修改的函数。在对某些部分修改之后,可以再次比较程序运行时间,查看优化结果。另外,这 份结果还包含一个特别有用的东西,那就是程序的动态函数调用情况,即程序运行过程中实际执行过的函数,这和calltree产生的静态调用树有所不同,它 能够反应程序在该次执行过程中的函数调用情况。而如果想反应程序运行的某一时刻调用过的函数,可以考虑采用gdb的backtrace命令。

    类似测试纸片厚度的方法,gprof也提供了一个统计选项,用于对程序的多次运行结果进行统计。另外,gprof有一个KDE下图形化接口kprof,这两部分请参考资料[3]。

    gprof虽然给出了函数级别的执行情况,但是如果想关心具体哪些条件分支被执行到,哪些语句没有被执行,该怎么办?

    1.4 代码覆盖率测试 gcov & ggcov

    如果要使用gcov,在编译时需要加上这两个选项 -fprofile-arcs -ftest-coverage,这里直接用之前的fib.c做演示。

    Quote:

    $ ls
    fib.c
    $ gcc -fprofile-arcs -ftest-coverage -o fib fib.c
    $ ls
    fib  fib.c  fib.gcno



    运行程序,并通过gcov分析代码的覆盖度
    Quote:

    $ ./fib
    $ gcov fib.c
    File 'fib.c'
    Lines executed:100.00% of 12
    fib.c:creating 'fib.c.gcov'



    12行代码100%被执行到,再查看分支情况,
    Quote:

    $ gcov -b fib.c
    File 'fib.c'
    Lines executed:100.00% of 12
    Branches executed:100.00% of 6
    Taken at least once:100.00% of 6
    Calls executed:100.00% of 4
    fib.c:creating 'fib.c.gcov'



    发 现所有函数,条件分支和语句都被执行到,说明代码的覆盖率很高,不过资料[3]gprof的演示显示代码的覆盖率高并不一定说明代码的性能就好,因为那些 被覆盖到的代码可能能够被优化成性能更高的代码。那到底那些代码值得被优化呢?执行次数最多的,另外,有些分支虽然都覆盖到了,但是这个分支的位置可能并 不是理想的,如果一个分支的内容被执行的次数很多,那么把它作为最后一个分支的话就会浪费很多不必要的比较时间。因此,通过覆盖率测试,可以尝试着剔除那 些从未执行过的代码,通过性能测试,可以找出那些值得优化的函数、分支或者是语句。

    如果使用-fprofile-arcs -ftest-coverage参数编译完代码,可以接着用-fbranch-probabilities参数对代码进行编译,这样,编译器就可以对根据代码的分支测试情况进行优化。

    Quote:

    $ wc -c fib
    16333 fib
    $ ls fib.gcda  #确保fib.gcda已经生成,这个是运行fib后的结果,-fbranch-probabilities一来它
    fib.gcda
    $ gcc -fbranch-probabilities -o fib fib.c #再次运行
    $ wc -c fib
    6604 fib
    $ time ./fib
    ...
    real    0m21.686s
    user    0m18.477s
    sys     0m0.008s



    可见代码量减少了,而且执行效率会有所提高,当然,这个代码效率的提高可能还跟其他因素有关,比如gcc还优化了一些很平台相关的指令。

    如 果想看看代码中各行被执行的情况,可以直接看fib.c.gcov文件。这个文件的各列依次表示执行次数、行号和该行的源代码。次数有三种情况,如果一直 没有执行,那么用####表示;如果该行注释、函数声明等,用-表示;如果是纯粹的代码行,那么用执行次数表示。这样我们就可以直接分析每一行的执行情 况。

    gprof也有一个图形化接口ggprof,是基于gtk+的,适合Gnome桌面的用户。

    现在都已经关注到代码行 了,实际上优化代码的前提是保证代码的正确性,如果代码还有很多bug,那么先要debug。不过下面的这些"bug"用普通的工具确实不太方便,虽然可 能,不过这里还是把它们归结为测试的内容,并且这里刚好承接上gcov部分,gcov能够测试到每一行的代码覆盖情况,而无论是内存访问越界、缓冲区溢出 还是内存泄露,实际上是发生在具体的代码行上的。

    1.5 内存访问越界 catchesegv, libSegFault.so

    "segmentation fault"是很头痛的一个问题,估计“纠缠”过很多人。这里仅仅演示通过catchsegv脚本测试段错误的方法,其他方法见资料[15]。

    catchsegv利用系统动态链接的PRELOAD机制(请参考man ld-linux),把库/lib/libSegFault.so提前load到内存中,然后通过它检查程序运行过程中的段错误。

    Quote:

    $ cat test.c
    #include <stdio.h>

    int main(void)
    {
            char str[10];

            sprintf(str, "%s", 111);

            printf("str = %s\n", str);
            return 0;
    }
    $ make test
    $ LD_PRELOAD=/lib/libSegFault.so ./test  #等同于catchsegv ./test
    *** Segmentation fault
    Register dump:

     EAX: 0000006f   EBX: b7eecff4   ECX: 00000003   EDX: 0000006f
     ESI: 0000006f   EDI: 0804851c   EBP: bff9a8a4   ESP: bff9a27c

     EIP: b7e1755b   EFLAGS: 00010206

     CS: 0073   DS: 007b   ES: 007b   FS: 0000   GS: 0033   SS: 007b

     Trap: 0000000e   Error: 00000004   OldMask: 00000000
     ESP/signal: bff9a27c   CR2: 0000006f

    Backtrace:
    /lib/libSegFault.so[0xb7f0604f]
    [0xffffe420]
    /lib/tls/i686/cmov/libc.so.6(vsprintf+0x8c)[0xb7e0233c]
    /lib/tls/i686/cmov/libc.so.6(sprintf+0x2e)[0xb7ded9be]
    ./test[0x804842b]
    /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe0)[0xb7dbd050]
    ./test[0x8048391]
    ...



    从 结果中可以看出,代码的sprintf有问题。经过检查发现它把整数当字符串输出,对于字符串的输出,需要字符串的地址作为参数,而这里的111则刚好被 解释成了字符串的地址,因此sprintf试图访问111这个地址,从而发生了非法访问内存的情况,出现"segmentation fault"。

    1.6 缓冲区溢出 libsafe.so

    缓 冲区溢出是指栈溢出(stack smashing),通常发生在对函数内的局部变量进行赋值操作时,超出了该变量的字节长度而引起对栈内原有数据(比如eip,ebp等)的覆盖,从而引 发内存访问越界,甚至执行非法代码,导致系统崩溃。关于缓冲区的详细原理和实例分析见资料[16]。这里仅仅演示该资料中提到的一种用于检查缓冲区溢出的 方法,它同样采用动态链接的PRELOAD机制提前装载一个名叫libsafe.so的库,你可以从这里获取它,http://www.sfr-fresh.com/linux/misc/libsafe-2.0-16.tgz,下载下来以后,再解压,编译,得到libsafe.so,

    下面,演示一个非常简单的,但可能存在缓冲区溢出的代码,并演示libsafe.so的用法。
    Quote:

    $ cat test.c
    $ make test
    $ LD_PRELOAD=/path/to/libsafe.so ./test ABCDEFGHIJKLMN
    ABCDEFGHIJKLMN
    *** stack smashing detected ***: ./test terminated
    Aborted (core dumped)



    资 料[6]分析到,如果不能够对缓冲区溢出进行有效的处理,可能会存在很多潜在的危险。虽然libsafe.so采用函数替换的方法能够进行对这类 stack smashing进行一定的保护,但是无法根本解决问题,alert7大虾在资料[17]中提出了突破它的办法,资料[18]提出了另外一种保护机制。

    1.7 内存泄露 Memwatch, Valgrind, mtrace

    堆 栈通常会被弄在一起叫,不过这两个名词却是指进程的内存映像中的两个不同的部分,栈(stack)用于函数的参数传递、局部变量的存储等,是系统自动分配 和回收的;而堆(heap)则是用户通过malloc等方式申请而且需要用户自己通过free释放的,如果申请的内存没有释放,那么将导致内存泄露,进而 可能导致堆的空间被用尽;而如果已经释放的内存再次被释放(double-free)则也会出现非法操作。(如果要真正理解堆和栈的区别,需要理解进程的 内存映像,请参考资料[22])

    这里演示通过Memwatch来检测程序中可能存在内存泄露,你可以从这里下载到这个工具,http://www.linkdata.se/sourcecode.html
    使用这个工具的方式很简单,只要把它链接(ld)到你的可执行文件中去,并在编译时加上两个宏开关-DMEMWATCH -DMW_STDIO。这里演示一个简单的例子。

    Quote:

    $ cat test.c
    #include <stdlib.h>
    #include <stdio.h>
    #include "memwatch.h"

    int main(void)
    {
      char *ptr1;
      char *ptr2;

      ptr1 = malloc(512);
      ptr2 = malloc(512);

      ptr2 = ptr1;
      free(ptr2);
      free(ptr1);
    }
    $ gcc -DMEMWATCH -DMW_STDIO test.c memwatch.c -o test
    $ cat memwatch.log
    ============= MEMWATCH 2.71 Copyright (C) 1992-1999 Johan Lindh =============

    Started at Sat Mar  1 07:34:33 2008

    Modes: __STDC__ 32-bit mwDWORD==(unsigned long)
    mwROUNDALLOC==4 sizeof(mwData)==32 mwDataSize==32

    double-free: <4> test.c(15), 0x80517e4 was freed from test.c(14)

    Stopped at Sat Mar  1 07:34:33 2008

    unfreed: <2> test.c(11), 512 bytes at 0x8051a14         {FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE ................}

    Memory usage statistics (global):
     N)umber of allocations made: 2
     L)argest memory usage      : 1024
     T)otal of all alloc() calls: 1024
     U)nfreed bytes totals      : 512



    通过测试,可以看到有一个512字节的空间没有被释放,而另外512字节空间却被连续释放两次(double-free)。valgrind和mtrace也可以做类似的工作,请参考资料[4]和mtrace的手册。

    2、代码调试

    调试的方法很多,调试往往要跟踪代码的运行状态,printf是最基本的办法,然后呢?静态调试方法有哪些,非交互的呢?非实时的有哪些?实时的呢?用于调试内核的方法有哪些?有哪些可以用来调试汇编代码呢?

    2.1 静态调试:printf + gcc -D(打印程序中的变量)

    利 用gcc的宏定义开关(-D)和printf函数可以跟踪程序中某个位置的状态,这个状态包括当前一些变量和寄存器的值。调试时需要用-D开关进行编译, 在正式发布程序时则可把-D开关去掉。这样做比单纯用printf方便很多,它可以避免清理调试代码以及由此带来的误删除代码等问题。

    Quote:

    $ cat test.c
    #include <stdio.h>
    #include <unistd.h>

    int main(void)
    {
            int i = 0;

    #ifdef DEBUG
            printf("i = %d\n", i);

            int t;
            __asm__ __volatile__ ("movl %%ebp, %0;":"=r"(t)::"%ebp");
            printf("ebp = 0x%x\n", t);
    #endif

            _exit(0);
    }
    $ gcc -DDEBUG -g -o test test.c
    $ ./test
    i = 0
    ebp = 0xbfb56d98



    上面演示了如何跟踪普通变量和寄存器变量的办法。跟踪寄存器变量采用了内联汇编,关于Linux下的汇编语言开发请参考资料[19]。

    不 过,这种方式不够灵活,我们无法“即时”获取程序的执行状态,而gdb等交互式调试工具不仅解决了这样的问题,而且通过把调试器拆分成调试服务器和调试客 户端适应了嵌入式系统的调试,另外,通过预先设置断点以及断点处需要收集的程序状态信息解决了交互式调试不适应实时调试的问题。

    2.2 交互式的调试(动态调试):gdb(支持本地和远程)/ald(汇编指令级别的调试)

    2.2.1 嵌入式系统调试方法 gdbserver/gdb

    估计大家已经非常熟悉GDB(Gnu DeBugger)了,所以这里并不介绍常规的gdb用法,而是介绍它的服务器/客户(gdbserver/gdb)调试方式。这种方式非常适合嵌入式系统的调试,为什么呢?先来看看这个:

    Quote:

    $ wc -c /usr/bin/gdbserver
    56000 /usr/bin/gdbserver
    $ which gdb
    /usr/bin/gdb
    $ wc -c /usr/bin/gdb
    2557324 /usr/bin/gdb
    $ echo "(2557324-56000)/2557324"  | bc -l
    .97810210986171482377


    gdb 比gdbserver大了将近97%,如果把整个gdb搬到存储空间受限的嵌入式系统中是很不合适的,不过仅仅5K左右的gdbserver即使在只有 8M Flash卡的嵌入式系统中也都足够了。所以在嵌入式开发中,我们通常先在本地主机上交叉编译好gdbserver/gdb。

    如果是初次使用这种方法,可能会遇到麻烦,而麻烦通常发生在交叉编译gdb和gdbserver时。在编译gdbserver/gdb前,需要配置(./configure)两个重要的选项:
  • --host,指定gdb/gdbserver本身的运行平台,
  • --target,指定gdb/gdbserver调试的代码所运行的平台,
    关 于运行平台,通过$MACHTYPE环境变量就可获得,对于gdbserver,因为要把它复制到嵌入式目标系统上,并且用它来调试目标平台上的代码,因 此需要把--host和--target都设置成目标平台;而gdb因为还是运行在本地主机上,但是需要用它调试目标系统上的代码,所以需要把-- target设置成目标平台。

    编译完以后就是调试,调试时需要把程序交叉编译好,并把二进制文件复制一份到目标系统上,并在本地需要保留一份源代码文件。调试过程大体如下,首先在目标系统上启动调试服务器:

    Quote:

    $ gdbserver :port /path/to/binary_file
    ...



    然后在本地主机上启动gdb客户端链接到gdb调试服务器,(gdbserver_ipaddress是目标系统的IP地址,如果目标系统不支持网络,那么可以采用串口的方式,具体看手册
    Quote:

    $ gdb
    ...
    (gdb) target remote gdbserver_ipaddress:2345
    ...



    其他调试过程和普通的gdb调试过程类似。

    2.2.2 汇编代码的调试 ald

    用gdb调试汇编代码貌似会比较麻烦,不过有人正是因为这个原因而开发了一个专门的汇编代码调试器,名字就叫做assembly language debugger,简称ald,你可以从这里下载到,http://ald.sourceforge.net/

    下载以后,解压编译后,我们来调试一个程序看看。

    这里是一段非常简短的汇编代码,摘自参考资料[20]



    Code:

    [Ctrl+A Select All]



    演示一下,

    Quote:

    //汇编、链接、运行
    $ as -o test.o test.s
    $ ld -o test test.o
    $ ./test "Hello World"
    Hello World
    //查看程序的入口地址
    $ readelf -h test | grep Entry
      Entry point address:               0x8048054
    //调试
    $ ald test
    ald> display
    Address 0x8048054 added to step display list
    ald> n
    eax = 0x00000000 ebx = 0x00000000 ecx = 0x00000001 edx = 0x00000000
    esp = 0xBFBFDEB4 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
    ds  = 0x007B es  = 0x007B fs  = 0x0000 gs  = 0x0000
    ss  = 0x007B cs  = 0x0073 eip = 0x08048055 eflags = 0x00200292

    Flags: AF SF IF ID

    Dumping 64 bytes of memory starting at 0x08048054 in hex
    08048054:  59 59 59 C6 41 0C 0A 31 D2 B2 0D 31 C0 B0 04 31    YYY.A..1...1...1
    08048064:  DB CD 80 31 C0 40 CD 80 00 2E 73 79 6D 74 61 62    ...1.@....symtab
    08048074:  00 2E 73 74 72 74 61 62 00 2E 73 68 73 74 72 74    ..strtab..shstrt
    08048084:  61 62 00 2E 74 65 78 74 00 00 00 00 00 00 00 00    ab..text........

    08048055                      59                   pop ecx



    可见ald在启动时就已经运行了被它调试的test程序,并且进入了程序的入口0x8048054,紧接着单步执行时,就执行了程序的第一条指令popl ecx。

    ald的命令很少,而且跟gdb很类似,比如
  • 这个几个命令用法和名字都类似 help,next,continue,set args,break,file,quit,disassemble,enable,disable等
  • 名字不太一样,功能对等的,examine对x, enter 对 set variable {int}地址=数据

    需 要提到的是:linux下的调试器包括上面的gdb和ald,以及strace等都用到了linux系统提供的ptrace()系统调用,这个调用为用户 访问内存映像提供了便利,如果想自己写一个调试器或者想hack一下gdb和ald,那么好好阅读资料[10]和man ptrace吧。

    2.3 实时调试:gdb tracepoint

    对 于程序状态受时间影响的程序,用上述普通的设置断点的交互式调试方法并不合适,因为这种方式将由于交互时产生的通信延迟和用户输入命令的时延而完全改变程 序的行为。所以gdb提出了一种方法以便预先设置断点以及在断点处需要获取的程序状态,从而让调试器自动执行断点处的动作,获取程序的状态,从而避免在断 点处出现人机交互产生时延改变程序的行为。

    这种方法叫tracepoints(对应breakpoint),它在gdb的user manual(见资料[21])里头有详细的说明,不过在gdb的官方发行版中至今都没有对它的实现。尽管如此,我们还是可以使用它,因为有其他组织做了 相关的工作,并以补丁的方式发布它。这个补丁你可以从这里获取ftp://dslab.lzu.edu.cn/pub/gdb_tracepoints

    获 取这个补丁以后,要做的就是把它patch到对应的gdb版本中,然后就是编译。因为tracepoints只定义在调试服务器和调试客户端这种方式中, 因此在这个实现中也是这样,如果想用它,你同样需要编译gdbserver和gdb,并类似嵌入式系统中的调试方法一样调试它。

    编译好以后通过ftp://dslab.lzu.edu.cn/pub/gdb_tracepoints/paper/tp.pdf和资料[21]就可以使用它。

    2.4 调试内核

    虽然这里并不会演示如何去hack内核,但是相关的工具还是需要简单提到的,资料[11]列出了绝大部分用于内核调试的工具,这些对你hack内核应该会有帮助的。

    3、代码优化

    除了资料[21]中的实践之外,我想我“应该”没有做过其他的项目优化工作吧,所以很遗憾,这里无法进行讨论了,不过我还是找了很多相关资料的,就让大家一起分享吧,这些资料都列在条目
  • 里。

    实际上呢?“代码测试”部分介绍的很多工具是为代码优化服务的,更多具体的细节请参考后面的资料,自己做实验吧。有任何相关的感兴趣的话题欢迎给我邮件zhangjinw@gmail.com。

    参考资料:

    [1] VERIFICATION AND VALIDATION
    http://satc.gsfc.nasa.gov/assure/agbsec5.txt
    [2] difference between verification and Validation
    http://www.faqs.org/qa/qa-9060.html
    [3] Coverage Measurement and Profiling(覆盖度测量和性能测试,Gcov and Gprof)
    http://www.linuxjournal.com/article/6758
    [4] Valgrind Usage
    A. Valgrind HOWTO
    http://www.faqs.org/docs/Linux-HOWTO/Valgrind-HOWTO.html
    B. Using Valgrind to Find Memory Leaks and Invalid Memory Use
    http://www.cprogramming.com/debugging/valgrind.html
    [5] MEMWATCH
    http://www.linkdata.se/sourcecode.html
    [6] Mastering Linux debugging techniques
    http://www.ibm.com/developerworks/linux/library/l-debug/
    [7] Software Performance Analysis
    http://arxiv.org/pdf/cs.PF/0507073.pdf
    [8] Runtime debugging in embedded systems
    http://dslab.lzu.edu.cn/docs/publications/runtime_debug.pdf
    [9] Tools Provided by System
    ltrace,mtrace,strace
    [10] Write your own debugger with the support ptrace()
    A.
    Process Tracing Using Ptrace
    http://linuxgazette.net/issue81/sandeep.html
    http://linuxgazette.net/issue83/sandeep.html
    B. Playing with ptrace
    http://www.linuxjournal.com/article/6100
    http://www.linuxjournal.com/node/6210/print
    http://www.ecos.sourceware.org/ml/libc-hacker/1998-05/msg00277.html
    [11] Kernel Debugging Relative Tools
    A. KGDB
    http://dslab.lzu.edu.cn/docs/publications/kernel_gdb.pdf
    B. GCOV
    http://linuxdevices.com/files/article062/der_herr_gcov.pdf
    C. KFI & KFT
    http://dslab.lzu.edu.cn/docs/publications/kfi.pdf
    D. UML(User Mode Linux)
    http://dslab.lzu.edu.cn/docs/publications/uml.pdf
    E. GDB Tracepoint
    http://dslab.lzu.edu.cn/docs/publications/tp.pdf
    F. Tools Collections
    http://dslab.lzu.edu.cn/docs/publications/tools.pdf
    G. Linux系统内核的调试
    http://www.ibm.com/developerworks/cn/linux/l-kdb/
    H. 嵌入式Linux内核调试技术
    http://www.eepw.com.cn/article/73300.htm
    [12] 用Graphviz进行可视化操作──绘制函数调用关系图
    http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1425.html
    [13] 用 Graphviz 可视化函数调用
    http://www.ibm.com/developerworks/cn/linux/l-graphvis/
    [14]
    介绍一个linux下生成C代码调用树的好工具calltree
    http://www.linuxsir.org/bbs/printthread.php?t=246389
    [15] 可恶的"Segmentation faults"之初级总结篇
    http://oss.lzu.edu.cn/blog/article.php?tid_700.html
    [16]
    Linux下缓冲区溢出攻击的原理及对策
    http://www.ibm.com/developerworks/cn/linux/l-overflow/index.html
    [17] 绕过libsafe的保护--覆盖_dl_lookup_versioned_symbol技术
    http://www.xfocus.net/articles/200208/423.html
    [18] 介绍Propolice怎样保护stack-smashing的攻击
    http://www.xfocus.net/articles/200103/78.html
    [19] Linux 汇编语言开发指南
    http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
    [20] 为你的可执行文件“减肥”
    http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1547.html
    [21] GDB Tracepoints
    http://sourceware.org/gdb/current/onlinedocs/gdb_11.html#SEC84
    [22] C语言程序缓冲区注入分析(第一部分:进程的内存映像)
    http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1539.html
  • Code Optimize Relatie
    A. Optimizing C Code
    http://www.jukie.net/~bart/slides/c-opt/c-opt.ps
    B. Performance programming for scientific computing
    http://www.research.ibm.com/perfprog/course/course.html
    C. Performance Programming
    http://www-cse.ucsd.edu/users/carter/perfprog.html
    D. Linux Profiling and Optimization
    http://www.cs.princeton.edu/picasso/mats/mats_S07/Lucifredi_Lecture_Feb07.pdf
    E. High-level code optimization
    http://web.abo.fi/~mats/codeopt2007/handouts/High-level-opt.pdf
    F. Code Optimization
    http://library.simugraph.com/articles/opti/optimizing.html
  • posted @ 2008-03-14 15:32 随意门 阅读(1314) | 评论 (0)编辑 收藏
    动态符号链接的细节

         摘要: by falcon<zhangjinw@gmail.com>2008-02-26        Linux支持动态连接库,不仅节省了磁盘、内存空间,而且可以提高程序运行效率[1]。不过引入动态连接库也可能会带来很多问题,例如动态连接库的调试 [4]、升级更新[5]和潜在的安全威胁[6][7]。这里主要讨论符号的动态链接过程...  阅读全文

    posted @ 2008-03-14 15:30 随意门 阅读(807) | 评论 (0)编辑 收藏
    C语言程序缓冲区注入的分析(第一部分:进程的内存映像)

         摘要: by falcon <zhangjinw@gmail.com>2008-2-13 闲言戏语    最近在写《Shell编程范例之进程操作》,到现在也没完。本打算介绍进程的相关操作,后面竟写到Linux下的C语言开发过程,想把文件是怎么变成进程 的整个过程给全部捣弄一遍。虽然到程序加载以及动态符号连接都已经很理解了,但是这伙却被进程的内存映像给...  阅读全文

    posted @ 2008-03-14 15:29 随意门 阅读(2006) | 评论 (0)编辑 收藏
    Linux命令行上程序执行的那一刹那!

    by falcon<zhangjinw@gmail.com>
    2008-02-15

        (这一小节应该是作为《shell编程范例之进程操作》的一些补充性质的内容。)

        当我们在Linux下的命令行输入一个命令之后,这背后发生了什么?

    1、什么是命令行接口

        用户使用计算机有两种常见的方式,一种是图形化的接口(GUI),另外一种则是命令行接口(CLI)。对于图形化的接口,用户点击某个图标就可启动后 台的某个程序;对于命令行的接口,用户键入某个程序的名字就可启动某个程序。这两者的基本过程是类似的,都需要查找程序文件在磁盘上的位置,加载到内存并 通过不同的解释器进行解析和运行。下面以命令行为例来介绍程序执行那一刹那发生的一些事情。
        首先来介绍什么是命令行?命令行就是command line,很直观的概念就是系统启动后的那个黑屏幕:有一个提示符,并有光标在闪烁的那样一个终端,一般情况下可以用CTRL+ALT+F1-6切换到不同的终端;在GUI界 面下也会有一些伪终端,看上去和系统启动时的那个终端没有什么区别,也会有一个提示符,并有一个光标在闪烁。就提示符和响应用户的键盘输入而言,它们两者 在功能上是一样的,实际上它们就是同一个东西,你用下面的命令就可以把它们打印出来。

    Quote:

    $ echo $SHELL   #打印当前SHELL,当前运行的命令行接口程序
    /bin/bash
    $ echo $$   #该程序对应的进程ID,$$是一个比较特殊的环境变量,它存放了当前进程ID
    1481
    $ ps -C bash   #通过PS命令查看
      PID TTY          TIME CMD
     1481 pts/0    00:00:00 bash


        从上面的操作结果可以看出,当前命令行接口实际上是一个程序,那就是/bin/bash,它是一个实实在在的程序,它打印提示符,接受用户输入的命令,分 析命令序列并执行然后返回结果。不过/bin/bash仅仅是当前使用的命令行程序之一,还有很多具有类似功能的程序,比如/bin/tcsh, bin/ash等。不过这里主要来讨论bash了,讨论它自己是怎么启动的,它怎么样处理用户的输入命令等后台细节?

    1.2 /bin/bash是什么时候启动的

    1.2.1 /bin/bash

       先通过CTRL+ALT+F1切换到一个普通终端下面,一般情况下看到的是XXX login: 提示输入用户名,接着是提示输入密码,然后呢?就直接登录到了我们的命令 行接口。实际上正是你输入正确的密码后,那个程序把/bin/bash给启动了。那是什么东西提示"XXX login:"的呢?正是/bin/login程序,那/bin/login程序怎么知道要启动/bin/bash,而不是其他的/bin/tcsh呢?
        /bin/login程序实际上会检查我们的/etc/passwd文件,在这个文件里头包含了用户名、密码和该用户的登录shell。密码和用户名匹配用户的登录,而登录shell则作为用户登录后的命令行程序。看看/etc/passwd中典型的这么一行:
    Quote:

    $ cat /etc/passwd | grep falcon
    falcon:x:1000:1000:falcon,,,:/home/falcon:/bin/bash


        这个是我用的帐号的相关信息哦,看到最后一行没?/bin/bash,这正是我登录用的命令行解释程序。至于密码呢,看到那个x没?这个x说明我的密码被 保存在另外一个文件里头/etc/shadow,而且密码是经过加密的。至于这两个文件的更多细节,看manual吧。
        我们怎么知道刚好是/bin/login打印了"XXX login"呢?现在回顾一下很早以前学习的那个strace命令。我们可以用strace命令来跟踪/bin/login程序的执行。
        跟上面一样,切换到一个普通终端,并切换到root用户,用下面的命令:
    Quote:

    $ strace -f -o strace.out /bin/login


        退出以后就可以打开strace.out文件,看看到底执行了哪些文件,读取了哪些文件。从中我们可以看到正是/bin/login程序用execve调 用了/bin/bash命令。通过后面的演示,我们发现/bin/login只是在子进程里头用execve调用了/bin/bash,因为在启动 /bin/bash后,我们发现/bin/login并没有退出。

    1.2.2 /bin/login

        那/bin/login又是怎么起来的呢?
        下面再来看一个演示。先在一个可以登陆的终端下执行下面的命令。
    Quote:

    $ getty 38400 tty8 linux


        getty命令停留在那里,貌似等待用户的什么操作,现在切回到第8个终端,是不是看到有"XXX login:"的提示了。输入用户名并登录,之后退出,回到第一个终端,发现getty命令已经退出。
        类似地,我们也可以用strace命令来跟踪getty的执行过程。在第一个终端下切换到root用户。执行如下命令,
    Quote:

    $ strace -f -o strace.out getty 38400 tty8 linux


        同样在strace.out命令中可以找到该命令的相关启动细节。比如,我们可以看到正是getty程序用execve系统调用执行了 /bin/login程序。这个地方,getty是在自己的主进程里头直接执行了/bin/login,这样/bin/login将把getty的进程空 间替换掉。

    1.2.3 /sbin/getty

        这里涉及到一个非常重要的东西了:/sbin/init,通过man init你可以查看到该命令的作用,它可是“万物之王”(init  is  the  parent of all processes on the system)哦。它是Linux系统默认启动的第一个程序,负责进行Linux系统的一些初始化工作,而这些初始化工作的配置则是通过 /etc/inittab来做的。那么来看看/etc/inittab的一个简单的example吧,可以通过man inittab查看相关帮助。
        整个配置文件的语法非常简单,就是下面一行的重复,
    Quote:

    id:runlevels:action:process


       
  • id就是一个唯一的编号,不用管它,一个名字而言,无关紧要。
       
  • runlevels是运行级别,整个还是比较重要的,理解运行级别的概念很有必要,它可以有如下的取值,
    Quote:

    0 is halt.
    1 is single-user.
    2-5 are multi-user.
    6 is reboot.


        不过,真正在配置文件里头用的是1-5了,而0和6非常特别,除了用它作为init命令的参数关机和重启外,似乎没有哪个“傻瓜”把它写在系统的配置文件 里头,让系统启动以后就关机或者重启。1代表单用户,而2-5则代表多用户。对于2-5可能有不同的解释,比如在slackware 12.0上,2,3,5被用来作为多用户模式,但是默认不启动X windows(GUI接口),而4则作为启动X windows的运行级别。
       
  • action是动作,它也有很多选择,我们关心几个常用的
        initdefault 用来指定系统启动后进入的运行级别,通常在/etc/inittab的第一条配置,如
    Quote:

            id:3:initdefault:


        这个说明默认运行级别是3,即多用户模式,但是不启动X window的那种。
        sysinit 指定那些在系统启动时将被执行的程序,例如
    Quote:

        si:S:sysinit:/etc/rc.d/rc.S
    [quote]
        在man inittab中提到,对于sysinit,boot等动作,runlevels选项是不用管的,所以我们可以很容易解读这条配置:它的意思是系统启动时 将默认执行/etc/rc.d/rc.S文件,在这个文件里你可直接或者间接的执行你想让系统启动时执行的任何程序,完成系统的初始化。
        wait,当进入某个特别的运行级别时,指定的程序将被执行一次,init将等到它执行完成,例如
    [quote]
    rc:2345:wait:/etc/rc.d/rc.M


        这个说明无论是进入运行级别2,3,4,5中哪一个,/etc/rc.d/rc.M将被执行一次,并且有init等待它执行完成。
        ctrlaltdel,当init程序接收到SIGINT信号时,某个指定的程序将被执行,我们通常通过按下CTRL+ALT+DEL,这个默认情况下将 给init发送一个SIGINT信号。如果我们想在按下这几个键时,系统重启,那么可以在/etc/inittab中写入,
    Quote:

    ca::ctrlaltdel:/sbin/shutdown -t5 -r now


        respawn,这个指定的进程将被重启,任何时候当它退出时。这意味着你没有办法结束它,除非init自己结束了。例如,
    Quote:

        c1:1235:respawn:/sbin/agetty 38400 tty1 linux


        这一行的意思非常简单,就是系统运行在级别1,2,3,5时,将默认执行/sbin/agetty程序(这个类似于上面提到的getty程序),这个程序非常有意思,就是无论什么时候它退出,init将再次启动它。这个有几个比较有意思的问题:
        在slackware 12.0下,当你把默认运行级别修改为4的时候,只有第6个终端可以用。原因是什么呢?因为类似上面的配置,因为那里只有1235,而没有4,这意味着当 系统运行在第4级别时,其他终端下的/sbin/agetty没有启动。所以,如果想让其他终端都可以用,把1235修改为12345即可。
        另外一个有趣的问题就是:正是init程序在读取这个配置行以后启动了/sbin/agetty,这就是我们的/sbin/agetty的秘密。
        还有一个问题:无论我们退出哪个终端,那个"XXX login:"总是会被打印,原因是respawn动作有趣的性质,因为它告诉init,无论/sbin/agetty什么时候退出,重新把它启动起来, 那跟"XXX login:"有什么关系呢?从前面的内容,我们发现正是/sbin/getty(同agetty)启动了/bin/login,而/bin/login 有启动了/bin/bash,即我们的命令行程序。
        而init程序作为“万物之王”,它是所有进程的“父”(也可能是祖父……)进程,那意味着其他进程最多只能是它的儿子进程。而这个子进程是怎么创建的, fork调用,而不是之前提到的execve调用。前者创建一个子进程,后者则会覆盖当前进程。因为我们发现/sbin/getty运行时,init并没 有退出,因此可以判断是fork调用创建一个子进程后,才通过execve执行了/sbin/getty。
        因此,我们可以总结出这么一个调用过程:
    Quote:

        fork   execve          execve         fork            execve 
    init --> init --> /sbin/getty --> /bin/login --> /bin/login --> /bin/bash


        这里的execve调用以后,后者将直接替换前者,因此当我们键入exit退出/bin/bash以后,也就相当于/sbin/getty都已经结束了, 因此最前面的init程序判断/sbin/getty退出了,又会创建一个子进程把/sbin/getty启动,进而又启动了/bin/login,又看 到了那个"XXX login:"。
        通过ps和pstree命令看看实际情况是不是这样,前者打印出进程的信息,后者则打印出调用关系。
    Quote:

    $ ps -ef | egrep "/sbin/init|/sbin/getty|bash|/bin/login"
    root         1     0  0 21:43 ?        00:00:01 /sbin/init
    root      3957     1  0 21:43 tty4     00:00:00 /sbin/getty 38400 tty4
    root      3958     1  0 21:43 tty5     00:00:00 /sbin/getty 38400 tty5
    root      3963     1  0 21:43 tty3     00:00:00 /sbin/getty 38400 tty3
    root      3965     1  0 21:43 tty6     00:00:00 /sbin/getty 38400 tty6
    root      7023     1  0 22:48 tty1     00:00:00 /sbin/getty 38400 tty1
    root      7081     1  0 22:51 tty2     00:00:00 /bin/login --      
    falcon    7092  7081  0 22:52 tty2     00:00:00 -bash


        我们过滤了一些不相干的数据。从上面的结果可以看到,除了tty2被替换成/bin/login外,其他终端都运行着/sbin/getty,说明终端2 上的进程是/bin/login,它已经把/sbin/getty替换掉,另外,我们看到-bash进程的父进程是7081刚好是/bin/login程 序,这说明/bin/login启动了-bash,但是它并没有替换掉/bin/login,而是成为了/bin/login的子进程,这说明 /bin/login通过fork创建了一个子进程并通过execve执行了-bash(后者通过strace跟踪到)。而init呢,其进程ID是1, 是/sbin/getty和/bin/login的父进程,说明init启动或者间接启动了它们。下面通过pstree来查看调用树,更清晰的看出上述关 系。
    Quote:

    $ pstree | egrep "init|getty|\-bash|login"
    init-+-5*[getty]
         |-login---bash
         |-xfce4-terminal-+-bash-+-grep


        结果显示init是5个getty程序,login程序和xfce4-terminal的父进程,而后两者则是bash的父进程,另外我们执行的grep命令则在bash上运行,是bash的子进程,这个将是我们后面关心的问题。
        从上面的结果发现,init作为所有进程的父进程,它的父进程ID饶有兴趣的是0,它是怎么被启动的呢?谁才是真正的“造物主”?

    1.2.4 谁启动了/sbin/init

        如果用过Lilo或者Grub这两个操作系统引导程序,你可能会用到Linux内核的一个启动参数init,当你忘记密码时,可能会把这个参数设置成/bin/bash,让系统直接进入命令行,而无须输入帐号和密码,这样就可以方便地登录密码修改掉。
        这个init参数是个什么东西呢?通过man bootparam会发现它的秘密,init参数正好指定了内核启动后要启动的第一个程序,而如果没有指定该参数,内核将依次查找/sbin/init, /etc/init, /bin/init, /bin/sh,如果找不到这几个文件中的任何一个,内核就要恐慌(panic)了,并呆在那里一动不动了。
        因此/sbin/init就是Linux内核启动的。而Linux内核呢?是通过Lilo或者Grub等引导程序启动的,Lilo和Grub都有相应的配 置文件,一般对应/etc/lilo.conf和/boot/grub/menu.lst,通过这些配置文件可以指定内核映像文件、系统根目录所在分区、 启动选项标签等信息,从而能够让它们顺利把内核启动起来。
        那Lilo和Grub本身又是怎么被运行起来的呢?还记得以前介绍的MBR不?MBR就是主引导扇区,一般情况下这里存放着Lilo和Grub的代码,而 谁知道正好是这里存放了它们呢?BIOS,如果你用光盘安装过操作系统的话,那么应该修改过BIOS的默认启动设置,通过设置你可以让系统从光盘、硬盘甚 至软盘启动。正是这里的设置让BIOS知道了MBR处的代码需要被执行。
        那BIOS又是什么时候被起来的呢?加电自检就执行到了这里。
        更多系统启动的细节,看看 "man boot-scripts" 吧。

        到这里,/bin/bash的神秘面纱就被揭开了,它只是系统启动后运行的一个程序而已,只不过这个程序可以响应用户的请求,那它到底是如何响应用户请求的呢?

    1.3 /bin/bash如何处理用户键入的命令

    1.3.0 预备知识

        在执行磁盘上某个程序时,我们通常不会指定这个程序文件的绝对路径,比如要执行echo命令时,我们一般不会输入/bin/echo,而仅仅是输入 echo。那为什么这样bash也能够找到/bin/echo呢?原因是Linux操作系统支持这样一种策略:shell的一个环境变量PATH里头存放 了程序的一些路径,当shell执行程序时有可能去这些目录下查找。which作为shell(这里特指bash)的一个内置命令,如果用户输入的命令是 磁盘上的某个程序,它会返回这个文件的全路径。

        有三个东西和终端的关系很大,那就是标准输入、标准输出和标准错误,它们是三个文件描述符,一般对应描述符0,1,2。在C语言程序里头,我们可以把它们 当作文件描述符一样进行操作。在命令行下,则可以使用重定向字符>,<等对它们进行操作。对于标准输出和标准错误,都默认输出到终端,对于标 准输入,也同样默认从终端输入。

    1.3.1 哪种命令先被执行

        在C语言里头要写一段输入字符串的命令很简单,调用scanf或者fgets就可以。这个在bash里头应该是类似的。但是它获取用户的命令以后,如何分析命令,如何响应不同的命令呢?
        首先来看看bash下所谓的命令,用最常见的test来作测试。
    Quote:

    $ test1   #随便键入一个字符串test1,bash发出响应,告诉我们找不到这个程序
    bash: test1: command not found
    $ test    #当我们键入test的时候,看不到任何输出,唯一的响应是,新的命令提示符被打印了
    $ type test
    test is a shell builtin
    #查看test这个命令的类型,即查看test将被如何解释,type告诉我们test是一个内置命令,如果没有理解错,test应该是利用诸如case "test": do something;break;这样的机制实现的。
    $ which test   #通过which查到/usr/bin下有一个test命令文件,在键入test时,到底哪一个被执行了呢?
    /usr/bin/test
    $ /usr/bin/test   #执行这个呢?也没什么反应,到底谁先被执行了?


        从上面的演示中发现一个问题?如果输入一个命令,这个命令要么就不存在,要么可能同时是shell的内置命令、也有可能是磁盘上环境变量PATH所指定的目录下的某个程序文件。
        考虑到test内置命令和/usr/bin/test命令的响应结果一样,我们无法知道哪一个先被执行了,怎么办呢?把/usr/bin/test替换成 一个我们自己的命令,并让它打印一些信息(比如hello,world!),这样我们就知道到底谁被执行了。写完程序,编译好,命名为test放到 /usr/bin下(记得备份原来那个)。开始测试:
    Quote:

    $ test #键入test,还是没有效果
    $ /usr/bin/test   #而键入绝对路径呢,则打印了hello, world!诶,那默认情况下肯定是内置命令先被执行了
    hello, world!


        总结:内置命令比磁盘文件中的程序优先被bash执行

        下面看看更多有趣的东西,键盘键入的命令还有可能是什么呢?因为bash支持别名和函数,所以还有可能是别名和函数,另外,如果PATH环境变量指定的不同目录下有相同名字的程序文件,那到底哪个被优先找到呢?
        下面再作一些实验,
    Quote:

    $ alias test="ls -l"   #把test命名为ls -l的别名
    $ test                 #再执行test,竟然执行了ls -l,而不是什么也没有,说明alias比内置命令更优先
    total 9488
    drwxr-xr-x 12 falcon falcon    4096 2008-02-21 23:43 bash-3.2
    -rw-r--r--  1 falcon falcon 2529838 2008-02-21 23:30 bash-3.2.tar.gz
    $ function test { echo "hi, I'm a function"; }   #定义一个名叫test的函数
    $ test   #执行一下,发现,还是执行了ls -l,说明function没有alias优先级高
    total 9488
    drwxr-xr-x 12 falcon falcon    4096 2008-02-21 23:43 bash-3.2
    -rw-r--r--  1 falcon falcon 2529838 2008-02-21 23:30 bash-3.2.tar.gz
    $ unalias test   #把别名给去掉
    $ test         #现在执行的是函数,说明函数的优先级比内置命令也要高
    hi, I'm a function
    $ builtin test #如果在命令之前跟上builtin,那么将直接执行内置命令
    $ unset test   #要去掉某个函数的定义,这样就可以


        通过这个实验我们得到一个命令的别名(alias)、函数(function),内置命令(builtin)和程序(program)的执行优先次序:
    Quote:

    先    alias --> function --> builtin --> program   后


        实际上,type命令会告诉我们这些细节,type -a会按照bash解析的顺序依次打印该命令的类型,而type -t则会给出第一个将被解析的命令的类型,之所以要做上面的实验,是为了让大家加印象。
    Quote:

    $ type -a test
    test is a shell builtin
    test is /usr/bin/test
    $ alias test="ls -l"
    $ function test { echo "I'm a function"; }
    $ type -a test
    test is aliased to `ls -l'
    test is a function
    test ()
    {
        echo "I'm a function"
    }
    test is a shell builtin
    test is /usr/bin/test
    $ type -t test
    alias


        下面再看看PATH指定的多个目录下有同名程序的情况。再写一个程序,打印“hi, world!”,以示和"hello, world!"的区别,放到PATH指定的另外一个目录/bin下,为了保证测试的说服力,再写一个放到另外一个叫/usr/local/sbin的目录 下。
        先看看PATH环境变量,确保它有/usr/bin,/bin和/usr/local/sbin这几个目录,然后通过type -P(-P参数强制到PATH下查找,而不管是别名还是内置命令等,可以通过help type查看该参数的含义)查看,到底哪个先被执行。

    Quote:

    $ echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
    $ type -P test   #可以看到/usr/local/sbin下的先被找到
    /usr/local/sbin/test
    $ rm /usr/local/sbin/test #把/usr/local/sbin/test下的给删除掉
    $ type -P test   #现在/usr/bin下的先被找到
    /usr/bin/test
    $ type -a test #type -a也显示类似的结果
    test is aliased to `ls -l'
    test is a function
    test ()
    {
        echo "I'm a function"
    }
    test is a shell builtin
    test is /usr/bin/test
    test is /bin/test


        可以找出这么一个规律:shell从PATH列出的路径中依次查找用户输入的命令。考虑到程序的优先级最低,如果想优先执行磁盘上的程序文件test呢?那么就可以用test -P找出这个文件并执行就可以了。

    补充:对于shell的内置命令,可以通过help command的方式获得帮助,对于程序文件,可以查看用户手册(当然,这个需要安装,一般叫做xxx-doc),man command。关于用户手册安装办法见在Linux下学会查看Man文档

    1.3.2 这些特殊字符是如何解析的:|, >, <, &

        在命令行上,除了输入各种命令以及一些参数外,比如上面type命令的各种参数-a, -P等,对于这些参数,是传递给程序本身的,非常好处理,比如if,else条件分之或者switch, case都可以处理。当然,在bash里头可能使用专门的参数处理函数getopt和getopt_long来处理它们。
        而|,>,<,&等字符,则比较特别,shell是怎么处理它们的呢?它们也被传递给程序本身吗?可我们的程序内部一般都不处理这些字符的,所以应该是shell程序自己解析了它们。
        先来看看这几个字符在命令行的常见用法,
    Quote:

    $ cat < ./test.c  #<字符表示:把test.c文件重定向为标准输入,作为cat命令的输入,而cat默认把内容输出到标准输出。
    #include <stdio.h>

    int main(void)
    {
            printf("hi, myself!\n");
            return 0;
    }
    $ cat < ./test.c > test_new.c #>表示把标准输出重定向为文件test_new.c,结果内容输出到test_new.c


        对于>,<,>>,<<,<>我们都称之为重定向(redirect),shell到底是怎么进行所谓的“重定向”的呢?
        这主要归功于dup/fcntl等函数,它们可以实现:复制文件描述符,让多个文件描述符共享同一个文件表项。比如,当把文件test.c重定向为标准输 入时。假设之前用以打开test.c的文件描述符是5,现在就把5复制为了0,这样当cat试图从标准输入读出内容时,也就访问了文件描述符5指向的文件 表项,接着读出了文件内容。输出重定向与此类似。其他的重定向,诸如>>, <<, <>等虽然和>,<的具体实现功能不太一样,但本质是一样的,都是文件描述符的复制,只不过可能对文件操作有一些附加的限制,比 如>>在输出时追加到文件末尾,而>则会从头开始写入文件,前者意味着文件的大小会增长,而后者则意味文件被重写。

        那么|呢?"|"被形象地称为“管道”,实际上它就是通过C语言里头的无名管道来实现的。先看一个例子,

    Quote:

    $ cat < ./test.c  | grep hi
            printf("hi, myself!\n");



        在这个例子中,cat读出了test.c文件中的内容,并输出到标准输出上,但是实际上输出的内容却只有一行,原因是这个标准输出被“接到”了grep命令的标准输入上,而grep命令只打印了包含“hi”字符串的一行。
        这是怎么被“接”上的。cat和grep作为两个单独的命令,它们本身没有办法把两者的输入和输出“接”起来。这正是shell自己的“杰作”,它通过C 语言里头的pipe函数创建了一个管道(一个包含两个文件描述符的整形数组,一个描述符用于写入数据,一个描述符用于读入数据),并且通过 dup/fcntl把cat的输出复制到了管道的输入,而把管道的输出则复制到了grep的输入。这真是一个奇妙的想法。
       
        那&呢?当你在程序的最后跟上这个奇妙的字符以后就可以接着做其他事情了,看看效果,
    Quote:

    $ sleep 50 &   #让程序在后台运行
    [1] 8261
    $ fg %1      #提示符被打印出来,可以输入东西,让程序到前台运行,无法输入东西了,按下CTRL+Z,再让程序到后台运行
    sleep 50

    [1]+  Stopped                 sleep 50
    $ fg %1   #再调到前台
    sleep 50


        实际上&正是shell支持作业控制的表征,通过作业控制,用户在命令行上可以同时作几个事情(把当前不做的放到后台,用&或者CTRL +Z或者bg)并且可以自由的选择当前需要执行哪一个(用fg调到前台)。这在实现时应该涉及到很多东西,包括终端会话(session)、终端信号、前 台进程、后台进程等。而在命令的后面加上&后,该命令将被作为后台进程执行,后台进程是什么呢?这类进程无法接收用户发送给终端的信号(如 SIGHUP,SIGQUIT,SIGINT),无法响应键盘输入(被前台进程占用着),不过可以通过fg切换到前台而享受作为前台进程具有的特权。
        因此,当一个命令被加上&执行后,shell必须让它具有后台进程的特征,让它无法响应键盘的输入,无法响应终端的信号(意味忽略这些信号),并 且比较重要的是新的命令提示符得打印出来,并且让命令行接口可以继续执行其他命令,这些就是shell对&的执行动作。

        还有什么神秘的呢?你也可以写自己的shell了,并且可以让内核启动后就执行它l,在lilo或者grub的启动参数上设置init= /path/to/your/own/shell/program就可以。当然,也可以把它作为自己的登录shell,只需要放到/etc/passwd 文件中相应用户名所在行的最后就可以。不过貌似到现在还没介绍shell是怎么执行程序,是怎样让程序变成进程的,所以继续。

    1.3.3 /bin/bash用什么魔法让一个普通程序变成了进程

        当我们从键盘键入一串命令,shell奇妙的响应了,对于内置命令和函数,shell自身就可以解析了(通过switch case之类的C语言语句)。但是,如果这个命令是磁盘上的一个文件呢。它找到该文件以后,怎么执行它的呢?
        还是用strace来跟踪一个命令的执行过程看看。
    Quote:

    $ strace -f -o strace.log /usr/bin/test
    hello, world!
    $ cat strace.log | sed -ne "1p"   #我们对第一行很感兴趣
    8445  execve("/usr/bin/test", ["/usr/bin/test"], [/* 33 vars */]) = 0


        从跟踪到的结果的第一行可以看到bash通过execve调用了/usr/bin/test,并且给它传了33个参数。这33个vars是什么呢?看看 declare -x的结果(这个结果只有32个,原因是vars的最后一个变量需要是一个结束标志,即NULL)。

    Quote:

    $ declare -x | wc -l   #declare -x声明的环境变量将被导出到子进程中
    32
    $ export TEST="just a test"   #为了认证declare -x和之前的vars的个数的关系,再加一个
    $ declare -x | wc -l
    33
    $ strace -f -o strace.log /usr/bin/test   #再次跟踪,看看这个关系
    hello, world!
    $ cat strace.log | sed -ne "1p"   
    8523  execve("/usr/bin/test", ["/usr/bin/test"], [/* 34 vars */]) = 0



        通过这个演示发现,当前shell的环境变量中被设置为export的变量被复制到了新的程序里头。不过虽然我们认为shell执行新程序时是在一个新的 进程里头执行的,但是strace并没有跟踪到诸如fork的系统调用(可能是strace自己设计的时候并没有跟踪fork,或者是在fork之后才跟 踪)。但是有一个事实我们不得不承认:当前shell并没有被新程序的进程替换,所以说shell肯定是先调用fork(也有可能是vfork)创建了一 个子进程,然后再调用execve执行新程序的。如果你还不相信,那么直接通过exec执行新程序看看,这个可是直接把当前shell的进程替换掉的。

    Quote:

    exec /usr/bin/test


        应该可以看到当前shell“哗”(听不到,突然没了而已)的一下就没有了。
        下面来模拟一下shell执行普通程序。multiprocess相当于当前shell,而/usr/bin/test则相当于通过命令行传递给shell的一个程序。这里是代码:



    Code:

    [Ctrl+A Select All]


        运行看看,
    Quote:

    $ make multiprocess
    $ ./multiprocess
    child: my pid is 2251
    child: my parent's pid is 2250
    hello, world!
    parent: my pid is 2250
    parent: wait for my child exit successfully!


        从执行结果可以看出,/usr/bin/test在multiprocess的子进程中运行并不干扰父进程,因为父进程一直等到了/usr/bin/test执行完成。
        再回头看看代码,你会发现execlp并没有传递任何环境变量信息给/usr/bin/test,到底是怎么把环境变量传送过去的呢?通过man exec我们可以看到一组exec的调用,在里头并没有发现execve,但是通过man execve可以看到该系统调用。实际上exec的那一组调用都只是libc库提供的,而execve才是真正的系统调用,也就是说无论使用exec调用 中的哪一个,最终调用的都是execve,如果使用execlp,那么execlp将通过一定的处理把参数转换为execve的参数。因此,虽然我们没有 传递任何环境变量给execlp,但是默认情况下,execlp把父进程的环境变量复制给了子进程,而这个动作是在execlp函数内部完成的。
        现在,总结一下execve,它有有三个参数,
        第一个是程序本身的绝对路径,对于刚才使用的execlp,我们没有指定路径,这意味着它会设法到PATH环境变量指定的路径下去寻找程序的全路径。
        第二个参数是一个将传递给被它执行的程序的参数数组指针。正是这个参数把我们从命令行上输入的那些参数,诸如grep命令的-v等传递给了新程序,可以通过main函数的第二个参数char *argv[]获得这些内容。
        第三个参数是一个将传递给被它执行的程序的环境变量,这些环境变量也可以通过main函数的第三个变量获取,只要定义一个char *env[]就可以了,只是通常不直接用它罢了,而是通过另外的方式,通过extern char **environ全局变量(环境变量表的指针)或者getenv函数来获取某个环境变量的值。
        当然,实际上,当程序被execve执行后,它被加载到了内存里,包括程序的各种指令、数据以及传递给它的各种参数、环境变量等都被存放在系统分配给该程序的内存空间中。
        我们可以通过/proc/<pid>/maps把一个程序对应的进程的内存映象看个大概。
    Quote:

    $ cat /proc/self/maps   #查看cat程序自身加载后对应进程的内存映像
    08048000-0804c000 r-xp 00000000 03:01 273716     /bin/cat
    0804c000-0804d000 rw-p 00003000 03:01 273716     /bin/cat
    0804d000-0806e000 rw-p 0804d000 00:00 0          [heap]
    b7c46000-b7e46000 r--p 00000000 03:01 87528      /usr/lib/locale/locale-archive
    b7e46000-b7e47000 rw-p b7e46000 00:00 0
    b7e47000-b7f83000 r-xp 00000000 03:01 466875     /lib/libc-2.5.so
    b7f83000-b7f84000 r--p 0013c000 03:01 466875     /lib/libc-2.5.so
    b7f84000-b7f86000 rw-p 0013d000 03:01 466875     /lib/libc-2.5.so
    b7f86000-b7f8a000 rw-p b7f86000 00:00 0
    b7fa1000-b7fbc000 r-xp 00000000 03:01 402817     /lib/ld-2.5.so
    b7fbc000-b7fbe000 rw-p 0001b000 03:01 402817     /lib/ld-2.5.so
    bfcdf000-bfcf4000 rw-p bfcdf000 00:00 0          [stack]
    ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


        关于程序加载和进程内存映像的更多细节请参考《C语言程序缓冲区注入分析》。

        到这里,关于命令行的秘密都被“曝光”了,可以开始写自己的命令行解释程序了。
        关于进程的相关操作请参考《shell编程范例之进程操作》。

    补充:上面没有讨论到一个比较重要的内容,那就是即使execve找到了某个可执行文件,如果该文件属主没有运行该程序的权限,那么也没有办法运行程序。可通过ls -l查看程序的权限,通过chmod添加或者去掉可执行权限。

  • 文件属主具有可执行权限时才可以执行某个程序
    Quote:

    whoami
    falcon
    $ ls -l hello  #查看用户权限(第一个x表示属主对该程序具有可执行权限
    -rwxr-xr-x 1 falcon users 6383 2000-01-23 07:59 hello*
    $ ./hello
    Hello World
    $ chmod -x hello  #去掉属主的可执行权限
    $ ls -l hello
    -rw-r--r-- 1 falcon users 6383 2000-01-23 07:59 hello
    $ ./hello
    -bash: ./hello: Permission denied


       
    参考的资料:

    [1] man boot-scripts
    Linux启动过程
    [2] man bootparam
    Linux内核启动参数
    [3] man 5 passwd
    [4] man shadow
    [5] "UNIX环境高级编程",进程关系一章
    [6] 2006,2007 Summer School hold by DSLab
    http://dslab.lzu.edu.cn/docs/2007summerschool/index.html
    http://dslab.lzu.edu.cn/docs/2006summerschool/index.html
  • posted @ 2008-03-14 15:27 随意门 阅读(3581) | 评论 (1)编辑 收藏
    GCC编译背后(第二部分:汇编和链接)

    (上接“GCC编译的背后(第一部分:预处理和编译)”)

    3、汇编

        开篇:这里实际上还是翻译过程,只不过把作为中间结果的汇编代码翻译成了机器代码,即目标代码,不过它还不可以运行。如果要产生这一中间结果,可用gcc的-c选项,当然,也可通过as命令_汇编_汇编语言源文件来产生。

        汇编是把汇编语言翻译成目标代码的过程,在学习汇编语言开发时,大家应该比较熟悉nasm汇编工具(支持Intel格式的汇编语言)了,不过这里主要用 as汇编工具来汇编AT&T格式的汇编语言,因为gcc产生的中间代码就是AT&T格式的。下面来演示分别通过gcc的-c选项和as来 产生 目标代码。

    Quote:

    $ file hello.s
    hello.s: ASCII assembler program text
    $ gcc -c hello.s        #用gcc把汇编语言编译成目标代码
    $ file hello.o            #file命令可以用来查看文件的类型,这个目标代码是可重定位的(relocatable),需                   #要通过ld进行进一步的链接成可执行程序(executable)和共享库(shared)
    hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
    $ as -o hello.o hello.s        #用as把汇编语言编译成目标代码
    $ file hello.o
    hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped



        gcc和as默认产生的目标代码都是ELF格式[6]的,因此这里主要讨论ELF格式的目标代码(如果 有时间再回顾一下a.out和coff格式,当然你也可以参考资料[15],自己先了解一下,并结合objcopy来转换它们,比较异同)。

        目标代码不再是普通的文本格式,无法直接通过文本编辑器浏览,需要一些专门的工具。如果想了解更多目标代码的细节,区分relocatable(可重定 位)、executable(可执行)、shared libarary(共享库)的不同,我们得设法了解目标代码的组织方式和相关的阅读和分析工具。下面我们主要介绍这部分内容。
        "BFD is a package which allows applications to use the same routines to operate on object files whatever the object file format. A new object file format can be supported simply by creating a new BFD back end and adding it to the library."[24][25]。
        binutils(GNU Binary Utilities)的很多工具都采用这个库来操作目标文件,这类工具有objdump,objcopy,nm,strip等(当然,你也可以利用它。如 果你深入了解ELF格式,那么通过它来分析和编写Virus程序将会更加方便),不过另外一款非常优秀的分析工具readelf并不是 基于这个库,所以你也应该可以直接用elf.h头文件中定义的相关结构来操作ELF文件。

        下面将通过这些辅助工具(主要是readelf和objdump,可参考本节最后列出的资料[4]),结合ELF手册[6](建议看第三篇中文版)来分析它们。

        下面大概介绍ELF文件的结构和三种不同类型ELF文件的区别。

    ELF文件的结构:

    ELF Header(ELF文件头)
    Porgram Headers Table(程序头表,实际上叫段表好一些,用于描述可执行文件和可共享库)
    Section 1
    Section 2   
    Section 3
    ...
    Section Headers Table(节区头部表,用于链接可重定位文件成可执行文件或共享库)

        对于可重定位文件,程序头是可选的,而对于可执行文件和共享库文件(动态连接库),节区表则是可选的。这里的可选是指没有也可以。可以分别通过 readelf文件的-h,-l和-S参数查看ELF文件头(ELF Header)、程序头部表(Program Headers Table,段表)和节区表(Section Headers Table)。

        文件头说明了文件的类型,大小,运行平台,节区数目等。先来通过文件头看看不同ELF的类型。为了说明问题,先来几段代码吧。



    Code:

    [Ctrl+A Select All]





    Code:

    [Ctrl+A Select All]





    Code:

    [Ctrl+A Select All]



        下面通过这几段代码来演示通过readelf -h参数查看ELF的不同类型。期间将演示如何创建动态连接库(即可共享文件)、静态连接库,并比较它们的异同。
    Quote:

    $ gcc -c myprintf.c test.c        #编译产生两个目标文件myprintf.o和test.o,它们都是可重定位文件(REL)
    $ readelf -h test.o | grep Type   
      Type:                              REL (Relocatable file)
    $ readelf -h myprintf.o | grep Type
      Type:                              REL (Relocatable file)
    $ gcc -o test myprintf.o test.o    #根据目标代码连接产生可执行文件,这里的文件类型是可执行的(EXEC)
    $ readelf -h test | grep Type
      Type:                              EXEC (Executable file)
    $ ar rcsv libmyprintf.a myprintf.o    #用ar命令创建一个静态连接库,静态连接库也是可重定位文件(REL)
    $ readelf -h libmyprintf.a | grep Type    #因此,使用静态连接库和可重定位文件一样,它们之间唯一不
                                            #同是前者可以是多个可重定位文件的“集合”。
      Type:                              REL (Relocatable file)
    $ gcc -o test test.o -llib -L./        #可以直接连接进去,也可以使用-l参数,-L指定库的搜索路径
    $ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0
                                        #编译产生动态链接库,并支持major和minor版本号,动态链接库类型为DYN
    $ ln -sf libmyprintf.so.0.0 libmyprintf.so.0
    $ ln -sf libmyprintf.so.0 libmyprintf.so
    $ readelf -h libmyprintf.so | grep Type
      Type:                              DYN (Shared object file)
    $ gcc -o test test.o -llib -L./        #编译时和静态连接库类似,但是执行时需要指定动态连接库的搜索路径
    $ LD_LIBRARY_PATH=./ ./test            #LD_LIBRARY_PATH为动态链接库的搜索路径
    $ gcc -static -o test test.o -llib -L./    #在不指定static时会优先使用动态链接库,指定时则阻止使用动态连接库
                                            #这个时候会把所有静态连接库文件加入到可执行文件中,使得执行文件很大
                                            #而且加载到内存以后会浪费内存空间,因此不建议这么做



        经过上面的演示基本可以看出它们之间的不同。可重定位文件本身不可以运行,仅仅是作为可执行文件、静态连接库(也是可重定位文件)、动态连接库的 “组件”。静态连接库和动态连接库本身也不可以执行,作为可执行文件的“组件”,它们两者也不同,前者也是可重定位文件(只不过可能是多个可重定位文件的 集合),并且在连接时加入到可执行文件中去;而动态连接库在连接时,库文件本身并没有添加到可执行文件中,只是在可执行文件中加入了该库的名字等信息,以 便在可执行文件运行过程中引用库中的函数时由动态连接器去查找相关函数的地址,并调用它们。从这个意义上说,动态连接库本身也具有可重定位的特征,含有可 重定位的信息。对于什么是重定位?如何进行静态符号和动态符号的重定位,我们将在链接部分和《动态符号链接的细节》一节介绍。

        下面来看看ELF文件的主体内容,节区(Section)。ELF文件具有很大的灵活性,它通过文件头组织整个文件的总体结构,通过节区表 (Section Headers Table)和程序头(Program Headers Table或者叫段表)来分别描述可重定位文件和可执行文件。但不管是哪种类型,它们都需要它们的主体,即各种节区。在可重定位文件中,节区 表描述的就是各种节区本身;而在可执行文件中,程序头描述的是由各个节区组成的段(Segment),以便程序运行时动态装载器知道如何对它们进行内存映像,从而方便程序加载和运行。
        下面先来看看 一些常见的节区,而关于这些节区(section)如何通过重定位构成成不同的段(Segments),以及有哪些常规的段,我们将在链接部分进一步介绍。

        可以通过readelf的-S参数查看ELF的节区。(建议一边操作一边看文档,以便加深对ELF文件结构的理解)先来看看可重定位文件的节区信息,通过节区表来查看:

    Quote:

    $ gcc -c myprintf.c            #默认编译好myprintf.c,将产生一个可重定位的文件myprintf.o
    $ readelf -S myprintf.o        #通过查看myprintf.o的节区表查看节区信息
    There are 11 section headers, starting at offset 0xc0:

    Section Headers:
      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            00000000 000000 000000 00      0   0  0
      [ 1] .text             PROGBITS        00000000 000034 000018 00  AX  0   0  4
      [ 2] .rel.text         REL             00000000 000334 000010 08      9   1  4
      [ 3] .data             PROGBITS        00000000 00004c 000000 00  WA  0   0  4
      [ 4] .bss              NOBITS          00000000 00004c 000000 00  WA  0   0  4
      [ 5] .rodata           PROGBITS        00000000 00004c 00000e 00   A  0   0  1
      [ 6] .comment          PROGBITS        00000000 00005a 000012 00      0   0  1
      [ 7] .note.GNU-stack   PROGBITS        00000000 00006c 000000 00      0   0  1
      [ 8] .shstrtab         STRTAB          00000000 00006c 000051 00      0   0  1
      [ 9] .symtab           SYMTAB          00000000 000278 0000a0 10     10   8  4
      [10] .strtab           STRTAB          00000000 000318 00001a 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
      I (info), L (link order), G (group), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)
    $ objdump -d -j .text   myprintf.o      #这里是程序指令部分,用objdump的-d选项可以看到反编译的结果,
                                                                            #-j指定需要查看的节区
    myprintf.o:     file format elf32-i386

    Disassembly of section .text:

    00000000 <myprintf>:
       0:   55                      push   %ebp
       1:   89 e5                   mov    %esp,%ebp
       3:   83 ec 08                sub    $0x8,%esp
       6:   83 ec 0c                sub    $0xc,%esp
       9:   68 00 00 00 00          push   $0x0
       e:   e8 fc ff ff ff          call   f <myprintf+0xf>
      13:   83 c4 10                add    $0x10,%esp
      16:   c9                      leave
      17:   c3                      ret
    $ readelf -r myprintf.o                         #用-r选项可以看到有关重定位的信息,这里有两部分需要重定位

    Relocation section '.rel.text' at offset 0x334 contains 2 entries:
     Offset     Info    Type            Sym.Value  Sym. Name
    0000000a  00000501 R_386_32          00000000   .rodata
    0000000f  00000902 R_386_PC32        00000000   puts
    $ readelf -x .rodata myprintf.o         #.rodata节区包含只读数据,即我们要打印的hello, world!.

    Hex dump of section '.rodata':
      0x00000000 68656c6c 6f2c2077 6f726c64 2100     hello, world!.

    $ readelf -x .data myprintf.o           #没有这个节区,.data应该包含一些初始化的数据

    Section '.data' has no data to dump.
    $ readelf -x .bss       mmyprintf.o             #也没有这个节区,.bss应该包含一些未初始化的数据,程序默认初始为0

    Section '.bss' has no data to dump.
    $ readelf -x .comment myprintf.o        #是一些注释,可以看到是是GCC的版本信息

    Hex dump of section '.comment':
      0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.
      0x00000010 3200                                2.
    $ readelf -x .note.GNU-stack myprintf.o #这个也没有内容

    Section '.note.GNU-stack' has no data to dump.
    $ readelf -x .shstrtab myprintf.o       #包括所有节区的名字

    Hex dump of section '.shstrtab':
      0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
      0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel.
      0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss.
      0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment
      0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack
      0x00000050 00                                  .

    $ readelf -symtab myprintf.o    #符号表,包括所有用到的相关符号信息,如函数名、变量名

    Symbol table '.symtab' contains 10 entries:
       Num:    Value  Size Type    Bind   Vis      Ndx Name
         0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
         1: 00000000     0 FILE    LOCAL  DEFAULT  ABS myprintf.c
         2: 00000000     0 SECTION LOCAL  DEFAULT    1
         3: 00000000     0 SECTION LOCAL  DEFAULT    3
         4: 00000000     0 SECTION LOCAL  DEFAULT    4
         5: 00000000     0 SECTION LOCAL  DEFAULT    5
         6: 00000000     0 SECTION LOCAL  DEFAULT    7
         7: 00000000     0 SECTION LOCAL  DEFAULT    6
         8: 00000000    24 FUNC    GLOBAL DEFAULT    1 myprintf
         9: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
    $ readelf -x .strtab myprintf.o #字符串表,用到的字符串,包括文件名、函数名、变量名等。

    Hex dump of section '.strtab':
      0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr
      0x00000010 696e7466 00707574 7300              intf.puts.



        从上表可以看出,对于可重定位文件,会包含这些基本节区.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。为了进一步理解这些节区和源代码的关系,这里来看一看myprintf.c产生的汇编代码。

    Quote:

    $ gcc -S myprintf.c
    $ cat myprintf.s
            .file   "myprintf.c"
            .section        .rodata
    .LC0:
            .string "hello, world!"
            .text
    .globl myprintf
            .type   myprintf, @function
    myprintf:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $8, %esp
            subl    $12, %esp
            pushl   $.LC0
            call    puts
            addl    $16, %esp
            leave
            ret
            .size   myprintf, .-myprintf
            .ident  "GCC: (GNU) 4.1.2"
            .section        .note.GNU-stack,"",@progbits



        是不是可以从中看出可重定位文件中的那些节区和汇编语言代码之间的关系?在上面的可重定位文件,可以看到有一个可重定位的节区,即. rel.text,它标记了两个需要重定位的项,.rodata和puts。这个节区将告诉编译器这两个信息在链接或者动态链接的过程中需要重定位, 具体如何重定位?将根据重定位项的类型,比如上面的R_386_32和R_386_PC32(关于这些类型的更多细节,请查看ELF手册[6])。

        到这里,对可重定位文件应该有了一个基本的了解,下面将介绍什么是可重定位,可重定位文件到底是如何被链接生成可执行文件和动态连接库的,这个过程除了进行了一些符号的重定位外,还进行了哪些工作呢?

    本节参考资料:

    [1] 了解编译程序的过程
    http://9iyou.com/Program_Data/linuxunix-3125.html
    http://www.host01.com/article/server/00070002/0621409075078127.htm
    [2] C track: compiling C programs.
    http://www.cs.caltech.edu/courses/cs11/material/c/mike/misc/compiling_c.html
    [3] Dissecting shared libraries
    http://www.ibm.com/developerworks/linux/library/l-shlibs.html

    4、链接

        开篇:重定位是将符号引用与符号定义进行链接的过程。因此链接是处理可重定位文件,把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。链接又 分为静态链接和动态链接,前者是程序开发阶段程序员用ld(gcc实际上在后台调用了ld)静态链接器手动链接的过程,而动态链接则是程序运行期间系 统调用动态链接器(ld-linux.so)自动链接的过程。比如,如果链接到可执行文件中的是静态连接库libmyprintf.a,那么. rodata节区在链接后需要被重定位到一个绝对的虚拟内存地址,以便程序运行时能够正确访问该节区中的字符串信息。而对于puts,因为它是动态连接库libc.so中定义的函数,所 以会在程序运行时通过动态符号链接找出puts函数在内存中的地址,以便程序调用该函数。在这里主要讨论静态链接过程,动态链接过程见《动态符号链接的细节》。

        静态链接过程主要是把可重定位文件依次读入,分析各个文件的文件头,进而依次读入各个文件的节区,并计算各个节区的虚拟内存位置,对一些需要重定位的符号 进 行处理,设定它们的虚拟内存地址等,并最终产生一个可执行文件或者是动态链接库。这个链接过程是通过ld来完成的,ld在链接时使用了一个链接脚本 (linker script), 该链接脚本处理链接的具体细节。由于静态符号链接过程非常复杂,特别是计算符号地址的过程,考虑到时间关系,相关细节请参考ELF手册[6]。这里主要介 绍可重定位文件中的节区(节区表描述的)和可执行文件中段(程序头描述的)的对应关系以及gcc编译时采用的一些默认链接选项。

        下面先来看看可执行文件的节区信息,通过程序头(段表)来查看:

    Quote:

    $ readelf -S test.o                        #为了比较,先把test.o的节区表也列出
    There are 10 section headers, starting at offset 0xb4:

    Section Headers:
      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            00000000 000000 000000 00      0   0  0
      [ 1] .text             PROGBITS        00000000 000034 000024 00  AX  0   0  4
      [ 2] .rel.text         REL             00000000 0002ec 000008 08      8   1  4
      [ 3] .data             PROGBITS        00000000 000058 000000 00  WA  0   0  4
      [ 4] .bss              NOBITS          00000000 000058 000000 00  WA  0   0  4
      [ 5] .comment          PROGBITS        00000000 000058 000012 00      0   0  1
      [ 6] .note.GNU-stack   PROGBITS        00000000 00006a 000000 00      0   0  1
      [ 7] .shstrtab         STRTAB          00000000 00006a 000049 00      0   0  1
      [ 8] .symtab           SYMTAB          00000000 000244 000090 10      9   7  4
      [ 9] .strtab           STRTAB          00000000 0002d4 000016 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
      I (info), L (link order), G (group), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)
    $ gcc -o test test.o libmyprintf.o
    $ readelf -l test        #我们发现,test和test.o,libmyprintf.o相比,多了很多节区,如.interp和.init等

    Elf file type is EXEC (Executable file)
    Entry point 0x80482b0
    There are 7 program headers, starting at offset 52

    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
      PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
      INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
          [Requesting program interpreter: /lib/ld-linux.so.2]
      LOAD           0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000
      LOAD           0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW  0x1000
      DYNAMIC        0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW  0x4
      NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
      GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

     Section to Segment mapping:
      Segment Sections...
       00    
       01     .interp
       02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
       03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
       04     .dynamic
       05     .note.ABI-tag
       06    



        上表给出了可执行文件的如下几个段(segment),

    PHDR: 给出了程序表自身的大小和位置,不能出现一次以上。
    INTERP: 因为程序中调用了puts(在动态链接库中定义),使用了动态连接库,因此需要动态装载器/链接器(ld-linux.so)
    LOAD: 包括程序的指令,.text等节区都映射在该段,只读(R)
    LOAD: 包括程序的数据,.data, .bss等节区都映射在该段,可读写(RW)
    DYNAMIC: 动态链接相关的信息,比如包含有引用的动态连接库名字等信息
    NOTE: 给出一些附加信息的位置和大小
    GNU_STACK: 这里为空,应该是和GNU相关的一些信息

        这里的段可能包括之前的一个或者多个节区,也就是说经过链接之后原来的节区被重排了,并映射到了不同的段,这些段将告诉系统应该如何把它加载到内存中。

        从上表中,通过比较可执行文件(test)中拥有的节区和可重定位文件(test.o和myprintf.o)中拥有的节区后发现,链接之后多了一些之前没有的节区,这些新的节区来自哪里?它们的作用是什么呢?先来通过gcc的-v参数看看它的后台链接过程。

    Quote:

    $ gcc -v -o test test.o myprintf.o    #把可重定位文件链接成可执行文件
    Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs
    Target: i486-slackware-linux
    Configured with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --host=i486-slackware-linux
    Thread model: posix
    gcc version 4.1.2
     /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../../i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crtn.o



        从上边的演示看出,gcc在连接了我们自己的目标文件test.o和myprintf.o之外,还连接了crt1.o,crtbegin.o等额外的目标文件,难道那些新的节区就来自这些文件?
        另外gcc在进行了相关配置(./configure)后,调用了collect2,却并没有调用ld,通过查找gcc文档中和collect2相关的部 分发现collect2在后台实际上还是去寻找ld命令的。为了理解gcc默认连接的后台细节,这里直接把collect2替换成ld,并把一些路径换成 绝对路径或者简化,得到如下的ld命令以及执行的效果。

    Quote:

    $ ld --eh-frame-hdr \
    -m elf_i386 \
    -dynamic-linker /lib/ld-linux.so.2 \
    -o test \
    /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o \
    test.o myprintf.o \
    -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed \
    /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o
    $ ./test
    hello, world!



    不出我们所料,它完美的运行了。下面通过ld的手册(man ld)来分析一下这几个参数。

    --eh-frame-hdr

    要求创建一个.eh_frame_hdr节区(貌似目标文件test中并没有这个节区,所以不关心它)。

  • -m elf_i386

    这 里指定不同平台上的链接脚本,可以通过--verbose命令查看脚本的具体内容,如ld -m elf_i386 --verbose,它实际上被存放在一个文件中(/usr/lib/ldscripts目录下),你可以去修改这个脚本,具体如何做?请参考ld的手册。 在后面我们将简要提到链接脚本中是如何预定义变量的,以及这些预定义变量如何在我们的程序中使用。需要提到的是,如果不是交叉编译,那么无须指定该选项。

  • -dynamic-linker /lib/ld-linux.so.2

    指定动态装载器/链接器,即程序中的INTERP段中的内容。动态装载器/连接器负责连接有可共享库的可执行文件的装载和动态符号连接。

  • -o test

    指定输出文件,即可执行文件名的名字

  • /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o

    链 接到test文件开头的一些内容,这里实际上就包含了.init等节区。.init节区包含一些可执行代码,在main函数之前被调用,以便进行一些初始化操 作,在C++中完成构造函数功能,更多细节请参考资料[9]

  • test.o myprintf.o

    链接我们自己的可重定位文件

  • -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed

    链接libgcc库和libc库,后者定义有我们需要的puts函数

  • /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o

    链接到test文件末尾的一些内容,这里实际上包含了.fini等节区。.fini节区包含了一些可执行代码,在程序退出时被执行,作一些清理工作,在C++中完成析构造函数功能。我们往往可以通过atexit来注册那些需要在程序退出时才执行的函数。

        对于crtbegin.o和crtend.o这两个文件,貌似完全是用来支持C++的构造和析构工作的[9],所以可以不链接到我们的可执行文件中,链接时把它们去掉看看,

    Quote:

    $ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o test.o myprintf.o -L/usr/lib -lc /usr/lib/crtn.o    #后面发现不用链接libgcc,也不用--eh-frame-hdr参数
    $ readelf -l test

    Elf file type is EXEC (Executable file)
    Entry point 0x80482b0
    There are 7 program headers, starting at offset 52

    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
      PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
      INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
          [Requesting program interpreter: /lib/ld-linux.so.2]
      LOAD           0x000000 0x08048000 0x08048000 0x003ea 0x003ea R E 0x1000
      LOAD           0x0003ec 0x080493ec 0x080493ec 0x000e8 0x000e8 RW  0x1000
      DYNAMIC        0x0003ec 0x080493ec 0x080493ec 0x000c8 0x000c8 RW  0x4
      NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
      GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

     Section to Segment mapping:
      Segment Sections...
       00    
       01     .interp
       02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
       03     .dynamic .got .got.plt .data
       04     .dynamic
       05     .note.ABI-tag
       06    
    $ ./test
    hello, world!



        完全可以工作,而且发现.ctors(保存着程序中全局构造函数的指针数组), .dtors(保存着程序中全局析构函数的指针数组),.jcr(未知),.eh_frame节区都没有了,所以crtbegin.o和crtend.o应该包含了这些节区。
        而对于另外两个文件crti.o和crtn.o,通过readelf -S查看后发现它们都有.init和.fini节区,如果我们不需要让程序进行一些初始化和清理工作呢?是不是就可以不 链接这个两个文件?试试看。

    Quote:

    $ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o test.o myprintf.o -L/usr/lib/ -lc
    /usr/lib/libc_nonshared.a(elf-init.oS): In function `__libc_csu_init':
    (.text+0x25): undefined reference to `_init'



    貌似不行,竟然有人调用了__libc_csu_init函数,而这个函数引用了_init。这两个符号都在哪里呢?

    Quote:

    $ readelf -s /usr/lib/crt1.o | grep __libc_csu_init
        18: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_csu_init
    $ readelf -s /usr/lib/crti.o | grep _init
        17: 00000000     0 FUNC    GLOBAL DEFAULT    5 _init



        竟然是crt1.o调用了__libc_csu_init函数,而该函数却引用了我们没有链接的crti.o文件中定义的_init符号。这样的话不链接 crti.o和crtn.o文件就不成了罗?不对吧,要不干脆不用crt1.o算了,看看gcc额外连接进去的最后一个文件crt1.o到底干了个啥子?

    Quote:

    $ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc
    ld: warning: cannot find entry symbol _start; defaulting to 00000000080481a4



        这样却说没有找到入口符号_start,难道crt1.o中定义了这个符号?不过它给默认设置了一个地址,只是个警告,说明test已经生成,不管怎样先运行看看再说。

    Quote:

    $ ./test
    hello, world!
    Segmentation fault



        貌似程序运行完了,不过结束时冒出个段错误?可能是程序结束时有问题,用gdb调试看看:

    Quote:

    $ gcc -g -c test.c myprintf.c    #产生目标代码, 非交叉编译,不指定-m也可以链接成功,所以下面可以去掉-m参数
    $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc
    ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8   
    $ ./test
    hello, world!
    Segmentation fault
    $ gdb ./test
    ...
    (gdb) l
    1       #include "test.h"
    2
    3       int main()
    4       {
    5               myprintf();
    6               return 0;
    7       }
    (gdb) break 7            #在程序的末尾设置一个断点
    Breakpoint 1 at 0x80481bf: file test.c, line 7.
    (gdb) r                    #程序都快结束了都没问题,怎么会到最后出个问题呢?
    Starting program: /mnt/hda8/Temp/c/program/test
    hello, world!

    Breakpoint 1, main () at test.c:7
    7       }
    (gdb) n                    #单步执行看看,怎么下面一条指令是0x00000001,肯定是程序退出以后出了问题
    0x00000001 in ?? ()
    (gdb) n                    #诶,当然找不到边了,都跑到0x00000001了
    Cannot find bounds of current function
    (gdb) c                    #原来是这么回事,估计是return 0返回之后出问题了,看看它的汇编去。
    Continuing.

    Program received signal SIGSEGV, Segmentation fault.
    0x00000001 in ?? ()
    $ gcc -S test.c #产生汇编代码
    $ cat test.s    #后面就这么几条指令,难不成ret返回有问题,不让它ret返回,把return改成_exit直接进入内核退出
    ...
            call    myprintf
            movl    $0, %eax
            addl    $4, %esp
            popl    %ecx
            popl    %ebp
            leal    -4(%ecx), %esp
            ret
    ...
    $ vim test.c
    $ cat test.c    #就把return语句修改成_exit了。
    #include "test.h"
    #include <unistd.h> /* _exit */

    int main()
    {
            myprintf();
            _exit(0);
    }
    $ gcc -g -c test.c myprintf.c
    $  ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc
    ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8
    $ ./test    #竟然好了,再看看汇编有什么不同
    hello, world!
    $ gcc -S test.c
    $ cat test.s    #貌似就把ret指令替换成了_exit函数调用,直接进入内核,然内核让处理了,那为什么ret有问题呢?
    ...
            call    myprintf
            subl    $12, %esp
            pushl   $0
            call    _exit
    ...
    $ gdb ./test    #把代码改回去(改成return 0;),再调试看看调用main函数返回时的下一条指令地址eip
    ...
    (gdb) l
    warning: Source file is more recent than executable.
    1       #include "test.h"
    2
    3       int main()
    4       {
    5               myprintf();
    6               return 0;
    7       }
    (gdb) break 5
    Breakpoint 1 at 0x80481b5: file test.c, line 5.
    (gdb) break 7
    Breakpoint 2 at 0x80481bc: file test.c, line 7.
    (gdb) r
    Starting program: /mnt/hda8/Temp/c/program/test

    Breakpoint 1, main () at test.c:5
    5               myprintf();
    (gdb) x/8x $esp    #发现0x00000001刚好是之前我们调试时看到的程序返回后的位置,即eip,说明程序在初始化的时候
                    #这个eip就是错误的。为什么呢?因为我们根本没有链接进来初始化的代码,而是在编译器自己给我们
                    #初始化了一个程序入口即00000000080481d8,也就是说,没有任何人调用main,main不知道返回哪里去
                    #所以,我们直接让main结束时进入内核调用_exit而退出则不会有问题
    0xbf929510:     0xbf92953c      0x080481a4      0x00000000      0xb7eea84f
    0xbf929520:     0xbf92953c      0xbf929534      0x00000000      0x00000001 



        通过上面的演示和解释发现只要把return语句修改为_exit语句,程序即使不链接任何额外的目标代码都可以正常运行(原因是不连接那些额外的文件时 相当于没有进行初始化操作,如果在程序的最后执行ret汇编指令,程序将无法获得正确的eip,从而无法进行后续的动作)。但是为什么会有“找不到 _start符号”的警告呢?通过readelf -s查看crt1.o发现里头有这个符号,并且crt1.o引用了main这个符号,是不是意味着会从_start进入main呢?是不是程序入口是 _start,而并非main呢?

        先来看看刚才提到的链接器的默认链接脚本(ld -m elf_386 --verbose),它告诉我们程序的入口(entry)是_start,而一个可执行文件必须有一个入口地址才能运行,所以这就是说明了为什么ld一 定要提示我们“_start找不到”,找不到以后就给默认设置了一个地址。

    Quote:

    $ ld --verbose  | grep ^ENTRY    #非交叉编译,可不用-m参数;ld默认找_start入口,并不是main哦!
    ENTRY(_start)



        原来是这样,程序的入口(entry)竟然不是main函数,而是_start。那干脆把汇编里头的main给改掉算了,看行不行?

    Quote:

    $ cat test.c
    #include "test.h"
    #include <unistd.h>     /* _exit */

    int main()
    {
            myprintf();
            _exit(0);
    }
    $ gcc -S test.c
    $ sed -i -e "s#main#_start#g" test.s    #把汇编中的main全部修改为_start,即修改程序入口为_start
    $ gcc -c test.s myprintf.c
    $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc    #果然没问题了 :-)
    $ ./test
    hello, world!



        _start竟然是真正的程序入口,那在有main的情况下呢?为什么在_start之后能够找到main呢?这个看看alert7大叔的"Before main分析"[5]吧,这里不再深入介绍。总之呢,通过修改程序的return语句为_exit(0)和修改程序的入口为_start,我们的代码不链接gcc默认链 接的那些额外的文件同样可以工作得很好。并且打破了一个学习C语言以来的常识:main函数作为程序的主函数,是程序的入口,实际上则不然。

        再补充一点内容,在ld的链接脚本中,有一个特别的关键字PROVIDE,由这个关键字定义的符号是ld的预定义字符,我们可以在C语言函数中扩展它们后直接使用。这些特别的符号可以通过下面的方法获取,

    Quote:

    $ ld --verbose | grep PROVIDE | grep -v HIDDEN
      PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS;
      PROVIDE (__etext = .);
      PROVIDE (_etext = .);
      PROVIDE (etext = .);
      _edata = .; PROVIDE (edata = .);
      _end = .; PROVIDE (end = .);


        这里面有几个我们比较关心的,第一个是程序的入口地址__executable_start,另外三个是etext,edata,end,分别对应程序的 代码段(text)、初始化数据(data)和未初始化的数据(bss)(可以参考资料[6]和man etext),如何引用这些变量呢?看看这个例子。


    Code:

    [Ctrl+A Select All]



        到这里,程序链接过程的一些细节都介绍得差不多了。在《动态符号链接的细节》中将主要介绍ELF文件的动态符号链接过程。

    本节参考资料

    [1] An beginners guide to compiling programs under Linux.
    http://www.luv.asn.au/overheads/compile.html
    [2] gcc manual
    http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/
    [3] A Quick Tour of Compiling, Linking, Loading, and Handling Libraries on Unix
    http://efrw01.frascati.enea.it/Software/Unix/IstrFTU/cern-cnl-2001-003-25-link.html
    [4] Unix 目标文件初探
    http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html
    [5] Before main()分析
    http://www.xfocus.net/articles/200109/269.html
    [6] A Process Viewing Its Own /proc/<PID>/map Information
    http://www.linuxforums.org/forum/linux-kernel/51790-process-viewing-its-own-proc-pid-map-information.html
  • posted @ 2008-03-14 15:24 随意门 阅读(7440) | 评论 (3)编辑 收藏
    GCC编译背后(第一部分:预处理和编译)

    by falcon <zhangjinw@gmail.com>

    平时在Linux下写代码,直接用"gcc -o out in.c"就把代码编译好了,但是这后面到底做了什么事情呢?如果学习过编译原理则不难理解,一般高级语言程序编译的过程莫过于:预处理、编译、汇编、链 接。gcc在后台实际上也经历了这几个过程,我们可以通过-v参数查看它的编译细节,如果想看某个具体的编译过程,则可以分别使用-E,-S,-c和- O,对应的后台工具则分别为cpp,cc1,as,ld。下面我们将逐步分析这几个过程以及相关的内容,诸如语法检查、代码调试、汇编语言等。

    1、预处理

        开篇简述:预处理是C语言程序从源代码变成可执行程序的第一步,主要是C语言编译器对各种预处理命令进行处理,包括头文件的包含、宏定义的扩展、条件编译的选择等。

        以前没怎么“深入”预处理,脑子对这些东西总是很模糊,只记得在编译的基本过程(词法分析、语法分析)之前还需要对源代码中的宏定义、文件包含、条件编译 等命令进行处理。这三类的指令很常见,主要有#define, #include和#ifdef ... #endif,要特别地注意它们的用法。(更多预处理的指令请查阅相关资料)

        #define除了可以独立使用以便灵活设置一些参数外,还常常和#ifdef ... #endif结合使用,以便灵活地控制代码块的编译与否,也可以用来避免同一个头文件的多次包含。关于#include貌似比较简单,通过man找到某个 函数的头文件,copy进去,加上<>就okay。这里虽然只关心一些技巧,不过预处理还是蕴含着很多潜在的陷阱(可参考<C Traps & Pitfalls>),我们也需要注意的。下面仅介绍和预处理相关的几个简单内容。

  • 打印出预处理之后的结果:gcc -E hello.c

        这样我们就可以看到源代码中的各种预处理命令是如何被解释的,从而方便理解和查错。

        实际上gcc在这里是调用了cpp的(虽然我们通过gcc的-v仅看到cc1),cpp即The C Preprocessor,主要用来预处理宏定义、文件包含、条件编译等。下面介绍它的一个比较重要的选项-D。

  • 在命令行定义宏:gcc -Dmacro hello.c

        这个等同于在文件的开头定义宏,即#define maco,但是在命令行定义更灵活。例如,在源代码中有这些语句。
    #ifdef DEBUG
    printf("this code is for debugging\n");
    #endif

        如果编译时加上-DDEBUG选项,那么编译器就会把printf所在的行编译进目标代码,从而方便地跟踪该位置的某些程序状态。这样-DDEBUG就可以当作一个调试开关,编译时加上它就可以用来打印调试信息,发布时则可以通过去掉该编译选项把调试信息去掉。

    本节参考资料:
    [1] C语言教程第九章:预处理
    http://www.bc-cn.net/Article/kfyy/cyy/jc/200409/9.html
    [2] 更多
    http://www.hemee.com/kfyy/c/6626.html
    http://www.91linux.com/html/article/program/cpp/20071203/8745.html
    http://www.janker.org/bbs/programmer/2006-10-13/327.html

    2、编译(翻译)

        开篇简要:编译之前,C语言编译器会进行词法分析、语法分析(-fsyntax-only),接着会把源代码翻译成中间语言,即汇编语言。如果想看到这个 中间结果,可以用-S选项。需要提到的是,诸如shell等解释语言也会经历一个词法分析和语法分析的阶段,不过之后并不会进行“翻译”,而是“解释”, 边解释边执行。

        把源代码翻译成汇编语言,实际上是编译的整个过程中的第一个阶段,之后的阶段和汇编语言的开发过程没有什么区别。这个阶段涉及到对源代码的词法分析、语法检查(通过-std指定遵循哪个标准),并根据优化(-O)要求进行翻译成汇编语言的动作。

        如果仅仅希望进行语法检查,可以用-fsyntax-only选项;而为了使代码有比较好的移植性,避免使用gcc的一些特性,可以结合-std和- pedantic(或者-pedantic-erros)选项让源代码遵循某个C语言标准的语法。这里演示一个简单的例子。

    Quote:

    $ cat hello.c
    #include <stdio.h>
    int main()
    {
            printf("hello, world\n")
            return 0;
    }
    $ gcc -fsyntax-only hello.c
    hello.c: In function ‘main’:
    hello.c:5: error: expected ‘;’ before ‘return’
    $ vim hello.c
    $ cat hello.c
    #include <stdio.h>
    int main()
    {
            printf("hello, world\n");
            int i;
            return 0;
    }
    $ gcc -std=c89 -pedantic-errors hello.c    #默认情况下,gcc是允许在程序中间声明变量的,但是turboc就不支持
    hello.c: In function ‘main’:
    hello.c:5: error: ISO C90 forbids mixed declarations and code



        语法错误是程序开发过程中难以避免的错误(人的大脑在很多条件下都容易开小差),不过编译器往往能够通过语法检查快速发现这些错误,并准确地告诉你语法错 误的大概位置。因此,作为开发人员,要做的事情不是“恐慌”(不知所措),而是认真阅读编译器的提示,根据平时积累的经验(最好在大脑中存一份常见语法错 误索引,很多资料都提供了常见语法错误列表,如<C Traps&Pitfalls>和最后面的参考资料[12]也列出了很多常见问题)和编辑器提供的语法检查功能(语法加亮、括号匹配提示 等)快速定位语法出错的位置并进行修改。

        语法检查之后就是翻译动作,gcc提供了一个优化选项-O,以便根据不同的运行平台和用户要求产生经过优化的汇编代码。例如,

    Quote:

    $ gcc -o hello hello.c            #采用默认选项,不优化
    $ gcc -O2 -o hello2 hello.c        #优化等次是2
    $ gcc -Os -o hellos hello.c        #优化目标代码的大小
    $ ls -S hello hello2 hellos        #可以看到,hellos比较小,hello2比较大
    hello2  hello  hellos
    $ time ./hello
    hello, world

    real    0m0.001s
    user    0m0.000s
    sys     0m0.000s
    $ time ./hello2                #可能是代码比较少的缘故,执行效率看上去不是很明显
    hello, world

    real    0m0.001s
    user    0m0.000s
    sys     0m0.000s

    $ time ./hellos                #虽然目标代码小了,但是执行效率慢了些
    hello, world

    real    0m0.002s
    user    0m0.000s
    sys     0m0.000s



        根据上面的简单演示,可以看出gcc有很多不同的优化选项,主要看用户的需求了,目标代码的大小和效率之间貌似存在一个“纠缠”,需要开发人员自己权衡。

        下面我们通过-S选项来看看编译出来的中间结果,汇编语言,还是以之前那个hello.c为例。
    Quote:

    $ gcc -S hello.c        #默认输出是hello.s,可自己指定,输出到屏幕-o -,输出到其他文件-o file
    $ cat hello.s
    cat hello.s
            .file   "hello.c"
            .section        .rodata
    .LC0:
            .string "hello, world"
            .text
    .globl main
            .type   main, @function
    main:
            leal    4(%esp), %ecx
            andl    $-16, %esp
            pushl   -4(%ecx)
            pushl   %ebp
            movl    %esp, %ebp
            pushl   %ecx
            subl    $4, %esp
            movl    $.LC0, (%esp)
            call    puts
            movl    $0, %eax
            addl    $4, %esp
            popl    %ecx
            popl    %ebp
            leal    -4(%ecx), %esp
            ret
            .size   main, .-main
            .ident  "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)"
            .section        .note.GNU-stack,"",@progbits



        不知道看出来没?和我们在课堂里学的intel的汇编语法不太一样,这里用的是AT&T语法格式。如果之前没接触过AT&T的,可以看看 参考资料[2]。如果想学习Linux下的汇编语言开发,从下一节开始哦,下一节开始的所有章节基本上覆盖了Linux下汇编语言开发的一般过程,不过这 里不介绍汇编语言语法。

        这里需要补充的是,在写C语言代码时,如果能够对编译器比较熟悉(工作原理和一些细节)的话,可能会很有帮助。包括这里的优化选项(有些优化选项可能在汇 编时采用)和可能的优化措施,例如字节对齐(可以看看这本书"Linux_Assembly_Language_Programming"的第六小节)、 条件分支语句裁减(删除一些明显分支)等。

    本节参考资料

    [1] Guide to Assembly Language Programming in Linux(pdf教程,社区有下载)
    http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=5&lid=94
    [2] Linux汇编语言开发指南(在线):
    http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
    [3] PowerPC 汇编
    http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html
    [4] 用于 Power 体系结构的汇编语言
    http://www.ibm.com/developerworks/cn/linux/l-powasm1.html
    [5] Linux Assembly HOWTO
    http://mirror.lzu.edu.cn/tldp/HOWTO/Assembly-HOWTO/
    [6] Linux 中 x86 的内联汇编
    http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html
    [7] Linux Assembly Language Programming
    http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Books

  • posted @ 2008-03-14 15:22 随意门 阅读(892) | 评论 (0)编辑 收藏
    把VIM打造成源代码的编辑器

    by falcon<zhangjinw@gmail.com>
    2008-02-22

        程序开发过程中,源代码的编辑主要是为了实现算法,结果则是一些可阅读的、便于检错的、可移植的...文本文件。如何产生一份良好的源代码文件,这不仅需要一些良好的编辑工具,还需要开发人员养成良好的编程修养[3][4]。

        Linux下有很多优秀的程序编辑工具,包括专门的文本编辑器和一些集成开发环境(IDE)中提供的编辑工具,前者的代表作有vim和emacs,后者的代表作则有Eclipse,Kdevelope,Anjuta等,这里主要介绍VIM的基本使用和一些基本配置。

        通过VIM进行文本编辑的一般过程(如附图二)包括:文件的打开、编辑、保存、关闭,而编辑则包括插入新内容、替换已有内容、查找内容,还包括复制、粘贴、删除等基本操作。

       

  • 打开文件

        在命令行下输入"vim+文件名"即可打开一个新的文件并进入VIM的“编辑模式”。编辑模式可以切换到命令模式(按下字符:)和插入模式(按下字母 a/A/i/I/o/O/s/S/c/C等或者Insert键)。编辑模式下,VIM会把键盘输入解释成VIM的编辑命令,以便实现诸如字符串查找(按下字母/)、文本复制(按下字母yy)、粘贴(按下字母pp)、删除(按下字母d等)、替换(s)等各种操作。当按下 a/A/i/I/o/O/s/S/c/C等字符时,VIM先执行这些字符对应的命令动作(比如移动光标到某个位置,删除某些字符),然后进入插入模式;进入插入模式后可以通过按下ESC键或者是CTRL+C返回到编辑模式,当然,在编辑模式下输入冒号":"后可进入命令模式,通过它可以完成一些复杂的编辑功能,比如进行正则表达式匹配替换,执行shell命令等。实际上,无论是插入模式还是命令模式都是编辑模式的一种。而编辑模式却并不止它们两个,还有字符串查找、删除、替换等。需要提到的是,如果在编辑模式按下字母v/V或者是CTRL+V,可以用光标选择一片代码,进而结合命令模式对这一片代码进行特定的操作。

       
  • 编辑文件

        打开文件以后即可进入编辑模式,这时可以进行各种编辑操作,包括插入、复制、删除、替换字符。其中两种比较重要的模式经常被“独立”出来,即上面提到的插入模式和命令模式。

       
  • 保存文件

        在退出之前需切换到命令模式,输入命令w以便保存各种编辑操作,如果想取消某种操作,可以用u命令。如果打开vim编辑器时没有设定文件名,那么在按下w命令时会提示没有文件名,此时需要在w命令后加上需要保存的文件名。

       
  • 退出

        保存好内容后就可退出,只需在命令模式下键入字符q。如果对文件内容进行了编辑,却没有保存,那么VIM会提示你保存内容,如果不想保存之前的编辑动作,那么可按下字符q并且在之后跟上一个感叹号!,这样会强制退出,不保存最近的内容变更。

        这里需要着重提到的是VIM的命令模式,它是VIM扩展各种新功能的接口,用户可以通过它启用和撤销某个功能,开发人员则可通过它为用户提供新的功能。下面主要介绍通过命令模式这个接口定制VIM以便我们更好地进行源代码的编辑(对于其他的内容建议看看参考资料中提到的VIM的官方教程和VIM实用技术序列,以及其他网友总结的VIM使用技巧等)。

        这里先提一下编码风格。刚学习编程时,代码写得很“难看”(不方便阅读,不方便检错,看不出任何逻辑结构),常常导致心情不好,而且排错也很困难,所以逐渐意识到代码编写需要规范,即养成良好的编码风格,如果换成俗话,那就是代码的排版,让代码好看一些。虽说“编程的“(高雅一些则称开发人员)不一定懂艺术,不过这个应该不是“搞艺术的”(高雅一些应该是文艺工作人员)的特权,而是我们应该具备的专业素养。在Linux下,比较流行的“行业”风格有KR的编码风格、gnu的编码风格、linux内核的编码风格(基于KR的,缩进是8个空格)等,它们都可以通过indent命令格式化,对应的选项分别是- kr,-gnu,-kr -i8。下面演示用indent把代码格式化成上面的三种风格。

    Quote:

    $ vim test.c
    $ cat test.c                  //这样糟糕的编码风格看者会让人想“哭”,太难阅读啦。
    cat test.c
    /* test.c -- a test program for using indent */
    #include<stdio.h>

    int main(int argc, char *argv[])
    {
     int i=0;
     if (i != 0) {i++; }
     else {i--; };
     for(i=0;i<5;i++)j++;
     printf("i=%d,j=%d\n",i,j);

     return 0;
    }
    $ indent -kr test.c
    $ cat test.c            //好看多了
    /* test.c -- a test program for using indent */
    #include<stdio.h>

    int main(int argc, char *argv[])
    {
        int i = 0;
        if (i != 0) {
            i++;
        } else {
            i--;
        };
        for (i = 0; i < 5; i++)
            j++;
        printf("i=%d,j=%d\n", i, j);
        return 0;
    }
    $ indent -gnu test.c
    $ cat test.c      //感觉不如kr的风格,处理if语句时增加了代码行,却并没明显改进效果
    /* test.c -- a test program for using indent */
    #include<stdio.h>

    int
    main (int argc, char *argv[])
    {
      int i = 0;
      if (i != 0)
        {
          i++;
        }
      else
        {
          i--;
        };
      for (i = 0; i < 5; i++)
        j++;
      printf ("i=%d,j=%d\n", i, j);
      return 0;
    }



        从演示中可看出编码风格真的很重要,但是如何养成良好的编码风格呢?经常练习,遵守某个编码风格,一如既往。不过这还不够,如果没有一个好编辑器,习惯也很难养成。而VIM提供了很多辅助我们养成良好编码习惯的功能,这些都通过它的命令模式提供。现在分开介绍几个功能;
    语法加“靓”(亮) :sytax on
    自动缩进宽度(需要set cin才有用):set sw=8
    TAB宽度:set ts=8
    显示行号;set number
    括号自动匹配;set sm
    C语言自动缩进:set cin

        这几个对代码编写来说非常有用,可以考虑把它们全部写到~/.vimrc文件(vim启动的时候会去执行这个文件里头的内容)中,如;
    Quote:

    $ vim ~/.vimrc
    $ cat ~/.vimrc
    :set number
    :set sw=8
    :set ts=8
    :set sm
    :set cin
    :syntax on
    :set textwidth=70
    :set mouse=a
    :set encoding=utf-8
    :set fileencoding=chinese
    :set fileencodings=ucs-bom, utf-8, chinese
    :set ambiwidth=double
    nmap <F2> :nohlsearch <CR>

    :abbr #b /***************************************************************************************

    :abbr #e ***************************************************************************************/



    需要补充的几个技巧有;

  • 在编辑模式下,可通过gqap命令对注释自动断行(每行字符个数可通过命令模式下的"set textwidth=个数"设定)
  • 命令模式下输入数字可以直接跳到指定行,也可在打开文件时用“vim +数字 文件名”实现相同的功能。
  • 命令模式下的TOhtml命令可把C语言输出为html文件,结合syntax on,可产生比较好的web page把代码发布出去。
  • 先切换到可视模式(编辑模式下按字母v可切换过来),用光标选中一片代码,然后通过命令模式下的命令“s#^#//#g"把某一片代码给注释掉,这非常方便调试某一片代码的功能。
  • 命令模式下的”set paste“可解决复制本来已有缩进的代码的自动缩进问题,后可执行”set nopaste“恢复自动缩进。
  • 为了使用最新的vim特性,可用"set nocp"取消与老版本的vi的兼容。
  • 如发现变量命名不好,想在整个代码中修改,可在命令模式下用"%s#old_variable#new_variable#g"全局替换。替换的时注意变量名是其他变量一部分的情况。
  • 如果想把缩进和TAB键替换成空格,可考虑设置expandtab,即“set et”,如果要把以前编写的代码中的缩进和TAB键都替换掉,可以用retab。
  • 为实现关键字补全,输入一部分字符后,按下CTRL+P即可。比如先输入prin,然后按下CTRL+P就可以补全了。
  • 如果想在在编辑模式下查看Linux手册,可把光标定位到在某个函数,按下Shift+k就可以调出man,很有用。
  • 删除空行,在命令模式下输入g/^$/d,前面g命令是扩展到全局,中间是匹配空行,后面d命令是执行删除动作。用替换也可以实现,键入%s#^\ n##g,意思是把所有以换行开头的行全部替换为空。类似地,如果要把多个空行转换为一个可以输入g/^\n$/d或者%s#^\n$##g。
  • 注意使用一些有用的插件,比如ctags, cscope等,可以提高代码阅读、分析的效率。特别是open source的东西。


    更多的技巧可以看看资料[2],[6],[7]。

        实际上,在源代码编写时还有很多需要培养的“素质”,例如源文件的开头注释、函数的注释,变量的命名等。这方面建议看看参考资料里的编程修养、内核编码风格、网络上流传的《华为编程规范》,以及<C Traps & Pitfalls>等。

    参考的资料:

    [1] VIM官方教程,在命令行下键入vimtutor即可
    [2] IBM developerworks 中国,VIM实用技术序列
    实用技巧,http://www.ibm.com/developerworks/cn/linux/l-tip-vim1/
    常用插件,http://www.ibm.com/developerworks/cn/linux/l-tip-vim2/
    定制VIM,http://www.ibm.com/developerworks/cn/linux/l-tip-vim3/
    [3] 编程修养
    http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=126&forum=13
    [4] 内核编码风格
    Document/codingstyle
    [5] Linux C编程(主要介绍了一些简单的技巧)
    http://blog.ednchina.com/brucedeng/4695/message.aspx
    [6] vim配置,配置得当可极大方便编程等工作
    http://oss.lzu.edu.cn/blog/article.php?tid_1398.html
    [7] VIM高级命令集锦
    http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=830&forum=6
    [8] C Traps & Pitfalls(英文版,不过比较简单呢)
    http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=6&lid=64
  • posted @ 2008-03-14 15:20 随意门 阅读(799) | 评论 (0)编辑 收藏
    Linux下C语言程序开发过的程视图

    by falcon<zhangjinw@gmail.com>
    2008-03-01

        到今天,关于"Linux下C语言开发过程"的一个简单视图总算粗略的完成了,从寒假之前的一段时间到现在过了将近一个月左右吧。写这个主题的目的源自 “shell编程范例之进程操作”,当我写到“shell编程范例之进程操作”这一节时,“突然”对进程的由来、本身和去向感到“迷惑不解”。所以想着好 好花些时间来弄清楚它们,现在发现,这个由来就是这里的程序开发过程,进程来自一个普通的文本文件,在这里是C语言程序,C语言程序经过编辑、预处理、编 译、汇编、链接、执行而成为一个进程;而进程本身呢?当一个可执行文件被执行以后,有了exec调用,被程序解释器映射到了内存中,有了它的内存映像;而 进程的去向呢?通过不断的执行指令和内存映像的变化,进程完成着各项任务,等任务完成以后就可以退出了(exit)。
        这样一份视图实际上是在寒假之前绘好的,你可以从附件中看到它;不过到现在才明白背后的很多细节。这些细节就是下面的这些blogs,你可以对照“视图”来阅读它们。
        1、把VIM打造成源代码编辑器(源代码编辑过程:用VIM编辑代码的一些技巧)
        2、GCC编译的背后 第一部分:预处理和编译 第二部分:汇编和链接(编译过程:预处理、编译、汇编、链接)
        3、程序执行的那一刹那 (执行过程:当我们从命令行输入一个命令之后)
        4、进程的内存映像 (进程加载过程:程序在内存里是个什么样子)
        5、动态符号链接的细节(动态链接过程:函数puts/printf的地址在哪里)
        6、代码测试、调试与优化小结(程序开发过后:内存溢出了吗?有缓冲区溢出?代码覆盖率如何测试呢?怎么调试汇编代码?有哪些代码优化技巧和方法呢?)
        7、为你的可执行文件“减肥”(从"减肥"的角度一层一层剖开ELF文件)
        8、进程和进程的基本操作(关于进程本身的相关操作,主要是介绍了一些shell命令)
        需要补充的是,“高等数学”(higher mathematics)、“线性代数”(linear algebra)、“数据结构”(data structure)、“数学建模”(mathematical modeling)、“设计模式”(design pattern)、“算法”(algorithm)、“离散数学”(discrete mathematics)、“数学分析”( mathematical analysis)等应该是程序设计必备的一些知识,在掌握相关工具的同时,这些相关的理论课程也需要很好的熟悉。
        欢迎大家一起交流和探讨。

    PS: 因为时间关系,很多blog都写得比较仓促,里头有错别字甚至是语义表达不清晰的地方,敬请原谅,我会逐步花时间进行检查的。

    推荐资料

    [1] mathematical modeling
    http://jpkc.nwu.edu.cn/sxjm/yxal.htm
    [2] design pattern
    [3] algorithm
    http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_338.html

    posted @ 2008-03-14 15:17 随意门 阅读(549) | 评论 (0)编辑 收藏
    Linux下非常命令学习

    引自 http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_62.html


    刚学linux的时候,有些东西不大熟悉,非常恼火
    为了脱离这话总困境,把自己遇到并解决的一些常用命令行操作集中写到这里

    1,如何删除非空目录?

    用rmdir吗?不是,而是

    #rm [your directory] -rf

    意思是强制删除该目录,以及该目录下所有文件,试试,肯定奏效,呵呵
    不过不要随便用,毫无提示就会删除掉的

    而rmdir只能删除空目录哦
    另外,如果不强制删除,只用
    #rm [your directory] -r

    2,压缩-解压缩命令大全

    tar.gz这个比较常见
    解压:tar zxvf FileName.tar.gz
    压缩:tar zcvf FileName.tar.gz DirName

    还在为面对一大堆的压缩文件无法解压缩而烦恼吗?
    这里有比较全面的信息哦
    http://www.chinaitlab.com/www/techspecial/tar/

    3,如何用命令行创建和删除文件名开头为"-"的文件?

    让我们来创建一个这样的文件“-test”
    #touch -test
    touch:日期格式 "est" 无效
    #touch -- -test
    #rm -test
    rm:无效选项 --t
    请尝试执行"rm --help"来获取更多帮助
    #rm -- test
    呵呵,是不是发现,只有加了"--"才可以正常操作阿

    4,如果,我在键入ls命令以后只想显示文件的部分信息,我该怎么办呢?
    也许你会查帮助ls --help
    可是那么多的组合确实是让人烦恼
    不过先在不用烦恼拉
    因为我们有gawk

    看看这个:ls -l | gawk '{printf $9}'
    看看输出什么出来拉
    是不是只有文件名拉
    要是我还要别的呢,那就在printf后面再加一个$x(x为1到9之间的字符哦)

    呵呵,其实gawk是一个脚本语言哦,功能非常强大,有兴趣看看相关的参考书去拉

    5,有个好东西,可以对linux服务进行相关的操作

    chkconf

    6,用rpm命令安装和卸载软件

    RPM共有10种基本的模式:它们是安装、查询、验证、删除等。

    安装模式:     rpm –i [安装选项] <软件包>
    查询模式:     rpm –q [查询选项]
    验证模式:     rpm –V 或 –verify [验证选项]
    删除模式:     rpm –e <软件包>

    7,tee命令

    这个命令的强大指处在于它会从标准输入设备读取数据,将其内容输出到标准输出设备,同时保存成文件。
    例如,我们想把一个文件inputfile的内容即输出到终端上也保存成outputfile1,outputfile2,那么我们就可以这么来弄:

    Quote:

    cat inputfile | tee outputfile1 outputfile2



    参考资料:
    http://jkwx007.blogchina.com/2514993.html
    http://jordi.blogbus.com/logs/2004/10/452282.html
    http://bbs.3671041.com/dispbbs.asp?boardid=9&id=747&star=1&page=1
    http://www.knowsky.com/print.asp?id=18403

    posted @ 2008-03-14 14:10 随意门 阅读(181) | 评论 (0)编辑 收藏
    几个shell程序设计小知识(shell常识部分)

    来源:http://www.chinaunix.net/jh/24/628479.html

    引用:一、用户登陆进入系统后的系统环境变量:
    $HOME 使用者自己的目录
    $PATH 执行命令时所搜寻的目录
    $TZ 时区
    $MAILCHECK 每隔多少秒检查是否有新的信件
    $PS1 在命令列时的提示号
    $PS2 当命令尚未打完时,Shell 要求再输入时的提示号
    $MANPATH man 指令的搜寻路径

    二、特殊变量:

    $0 这个程序的执行名字
    $n 这个程序的第n个参数值,n=1..9
    $* 这个程序的所有参数
    $# 这个程序的参数个数
    $$ 这个程序的PID
    $! 执行上一个指令的PID
    $? 执行上一个指令的返回值

    三、shell中的变元:
    * 任意字符串
    ? 一个任意字符
    [abc] a, b, c三者中之一
    [a-n] 从a到n的任一字符

    四、几个特殊字符表示

    \b 退回
    \c 打印一行时没有换行符 这个我们经常会用到
    \f 换页
    \r 回车
    \t 制表
    \v 垂直制表
    \\ 反斜线本身

    五、判断文件的属性

    格式:-操作符 filename
    -e 文件存在返回1, 否则返回0
    -r 文件可读返回1,否则返回0
    -w 文件可写返回1,否则返回0
    -x 文件可执行返回1,否则返回0
    -o 文件属于用户本人返回1, 否则返回0
    -z 文件长度为0返回1, 否则返回0.
    -f 文件为普通文件返回1, 否则返回0
    -d 文件为目录文件时返回1, 否则返回0

    六、测试字符串
    字符串1 = 字符串2 当两个字串相等时为真
    字符串1 != 字符串2 当两个字串不等时为真
    -n 字符串      当字符串的长度大于0时为真
    -z 字符串      当字符串的长度为0时为真
    字符串       当串字符串为非空时为真

    七、测试两个整数关系
    数字1 -eq 数字2     两数相等为真
    数字1 -ne 数字2     两数不等为真
    数字1 -gt 数字2     数字1大于数字2为真
    数字1 -ge 数字2     数字1大于等于数字2为真
    数字1 -lt 数字2     数字1小于数字2为真
    数字1 -le 数字2     数字1小于等于数字2为真

    八、逻辑测试
    -a         与
    -o        或
    !        非



    今天介绍shell特殊字符的引用
    ===============================
    shell中的特殊字符有

    1、$ 美元符
    2、\ 反斜杠
    3、` 反引号
    4、" 双引号
    5、< ,>,*,?,[,]

    下面我一一举列说明
    一、$符号
    1、echo $? 显示的是上一条指令退出状态
    2、echo "$?" 效果同上
    3、echo '$?' 显示的是$?
    4、echo \$? 显示的是$?
    5、echo "\$?" 显示的是$?

      大家可能已经看出 $符号在双引号中具有特殊意义 双引号对$符号不起作用
    而单引号可以将特殊字符的的特殊意义屏蔽掉,使其能显示为字符本身,反斜
    杠也可以将特殊字符的特殊含义屏蔽掉,使特殊字符失去特殊含义。

    二、\ 反斜杠
      反斜杠的作用是将特殊符号字符的特殊含义屏蔽掉,使其还是原字符
    A=1234
    echo \$A 显示为$A 如果不加\将显示为1234
    echo \` 显示为`
    echo \" 显示为双引号
    echo \\ 显示为\

    三、` 反引号
      反引号的功能是命令替换,将反引号中的字符串做为命令来执行,我们在用shell编程时经常用的到 将系统命令的执行结果赋给一个变量

    A=`date`
    echo $A 显示的不是date而是当时的时间串
    比如有一文件A的内容如下 
    ABCDEFG
    1234456
    abcdefg

    B=`cat A|grep 234` # 检索文件A中含有字符串234的行
    echo $B 将显示为1234456
    echo "$B" 将显示为什么?
    echo "\$B" 将显示为什么?读者自己试试

    四、" 双引号
      在系统中有些特殊字符,为避免引用这些特殊字符 往往用双引号或单引号将这些特殊字符引起来,使其不具有特殊含义。
      但有一部分特殊字符在引号中还是具有特殊含义,用双引号引起来是不起作用的。本文中所列的前四个特殊字符在双引号中还是特殊字符。为了使其不具有特殊含义一是用单引号引进来二是用\反斜线使其失去作用。

      比如我们想原样输出这些特殊字符

    echo """
    echo "$"
    echo "\"
    echo "`"
       以上不是你所期望的结果,因为双引号对它们不起作用,你只能这样才能输出这些特殊字符的原形
    echo '"'
    echo '$'
    echo '\'
    echo '`'

    echo "\""
    echo "\$"
    echo "\\"
    echo "\`"
    将分别显示为 " $ \ `
    五、其它特殊字符
      大家注意到 除了前四个特殊字符外 我将其它的特殊字符都放在一块,这是因为前四个特殊字符在双引号中还是具有特殊含义,所以单独拿出来讲,除此以外的特殊字符如果你要输出这些特殊字符的原形,你就可以用双引号或单引号引起来使其失去特殊含义。
    < ,>,*,?,[,]对shell有特殊含义 但你可以用双引号引起来输入这些原形

      讲了这么多大家是不是已经注意到所有的特殊字符在单引号中失去特殊含义,如果你要输出特殊字符原形但又记不清那些特殊字符在双引号中不能输出原形,建议你干脆用单引号引起来。

    今天介绍条件测试语句

    一、if 条件语句 
    格式:
    if 条件表达式
    then #当条件为真时执行以下语句
    命令列表
    else #为假时执行以下语句
    命令列表
    fi

    if 语句也可以嵌套使用

    if 条件表达式1
    then
    if 条件表达式2
    then
    命令列表
    else
    if 条件表达式3
    then
    命令列表
    else
    命令列表
    fi
    fi
    else
    命令列表
    fi

    你可以进行多层嵌套 一个if语句一定要跟一个fi 表示该层条件结束  否则会造成语法错误
    结合前面讲的 举例如下:
    这里先讲一个条件语句中用到的命令test 表示测试test后面的条件是否为真

    if test -f "$1"
    then
    lpr $1
    else
    if test -d "$1"
    then
    cd $1
    lpr $1
    else
    echo "$1不是文件或目录"
    fi
    fi

    以上的例子还可以改成如下所示

    if test -f "$1"
    then
    lpr $1
    elif test -d "$1" #elif 同else if
    then
    (cd $1;lpr $1)
    else
    echo "$1不是文件或目录"
    fi

    以上的例子不知您是否看懂是什么意思吗?
    假如我们现在将这个例子保存为prfile
    chmod +x prfile
    执行刚才的程序
    ./prfile aaa

    这个例子是检查你的输入的参数是否是一个文件 如果是就打印 如果是一个目录 先转目录再打印 如果即不是文件也不是目录给出提示

    二、多重条件测试语句case
    格式:
    case 字串 in
    模式) 命令列表;;
    模式) 命令列表;;
    ....
    esac

    多重条件语句是以case 开始以esac结束 中间可以有多个条件列表 功能是测试字串和和里面的模式有没有匹配的,有就执行里面的命令列表 模式也可以是*号 表示任意字串,每个模式里面的最后要心;;双引号结束,否则会发生语法错误。

    现举例如下:

    case $1 in
    *.c)
    cc $1
    ;;
    *.txt)
    lpr $1
    ;;
    *)
    echo "未知的类型"
    esac

    假如将以上内容保存在文件abc中

    chmod +x abc
    执行 ./abc a.c   将会对文件a.c进行编译
    执行 ./abc readme.txt 将会把文件通过打印机
    假如我将以上内容改一下,你是否会知道它的执行结果?

    case $1 in
    *)
    cc $1
    ;;
    *.txt)
    lpr $1
    ;;
    *.c)
    echo "未知的类型"
    esac

    今天介绍循环语句
    一. while 循环
    while 命令格式

    while 条件表
    do
    命令表
    done

    执行过程

    shell首先执行条件表,如果条件表的最后一条语句的退出状态为零,则执行盾环体内的命令
    表,执行完后,再检查条件表,如果退出状态为零将继续执行,如此循环往复直到条件表的
    最后一条语句的退出状态非零. 退出状态为零就是条件为真True.

    举例说明 假如shell文件的内容如下:

    Sum=0
    i=0
    while true #true是系统的关键词 表示真
    do
    i=`expr $i + 1`
    Sum=`expr $Sum + $i`
    if [ $i = "100" ]
    then
    break;
    fi
    done
    echo $i $Sum
    最后这个程序显示的是 100 5050
    这个程序的运算就是将1到100加起来

    下面将这个程序再改动一下


    Sum=0
    i=0
    while [ $i != "100" ]
    do
    i=`expr $i + 1`
    Sum=`expr $Sum + $i`
    done
    echo $i $Sum

    改动后的程序运算结果和上面是一样 但程序比上面的要简练

    在这个循环中还可以以until做为测试条件 它正好与while测试的条件相反,也就是当条件为假时将继续执行循环体内的语句,否则就退出循环体,下面还用这个例子.


    Sum=0
    i=0
    until [ $i = "100" ]
    do
    i=`expr $i + 1`
    Sum=`expr $Sum + $i`
    done
    echo $i $Sum
    当i不等于100时循环 就是当条件为假时循环,否则就退出,而第一个例子是当i不等于100
    时循环,也就是测试条件为真时循环.

    二.for 循环

    命令格式:
    for 变量 in 名字列表
    do
    命令列表
    done

    这里的名字列表是一个由空格分隔的字符串列表,shell在执行for循环时每次依次从名字表
    中取出一个字符串赋给循环变量作为变量的值.
    在写for语句时,也可以省略in 名字列表部分,这表示用当前的位置参数来代替这时的名
    字列表.
    下面举个例子
    比如在你的电脑中有两个目录,一个是aa,一个是bb在这两个目录中有5个相同的文件,但其
    中一个目录中的一个或多个文件刚刚修改过,现在我忘记刚才改的是那几个文件 了,那么我靠梢员冉弦幌抡饬礁瞿柯嫉奈募椭懒?程序如下:

    for File in a1 a2 a3 a4 a5
    do
    diff aa/$File bb/$File
    done

    下面再举一个不带名字列表的例子

    for File
    do
    echo $Filw
    done

    文件内容保存在a.sh中 并可执行
    我们在执行这个shell程序时命令行如下:
    a.sh a1 a2 a3 a4 a5
    执行结果如下:
    a1
    a2
    a3
    a4
    a5
    大家从这个例子中可以看到命令行的参数被逐一读入一次
    三.循环控制语句
    break 命令不执行当前循环体内break下面的语句从当前循环退出.
    continue 命令是程序在本循体内忽略下面的语句,从循环头开始执行.

    一,命令组合:圆括号和花括号
    shell中有两种方法将命令组合在一起:圆括号和花括号.圆括号使shell创建一个子shell
    来读取并执行括起来的名命令.左括号和右括号不论出现在命令行中的什么位置,shell都会
    认为它们具有特殊的组合意义的.只有用双引号将它们括起来引用,才表示圆括号或花括号
    的原义.例如:

    echo a(b)
    将出现语法上的错误,要想输出a(b)字符串 只能括起来
    echo "a(b)"
    或echo a"("b")"
    这样才能被shell正确解释.
    利用组合命令有什么作用呢?
    一,用圆括号组合命令
    圆括号的组合命令可以创建子进程运行组合程序,建立子进程的功能是很有用的,因为
    子shell在组合命令中的种种操作都不会影响到当前shell的各变量的值.
    例如:
    子进程在执行组合命令时改变了工作目录,并在新的工作目录下执行一系例命令,执行
    完后它可以不必返回原工作目录,因为子进程工作目录的改变不会影响到当前工作目录.

    创建子进程后将当前的环境也同样传给子shell,当前shell中用export输出到环境中的
    各变量在子shell中同样有效.


    花括号也可以将命令组合在一起.左 右花括号只有作为一条命令的第一个字出现时,
    shell才它们含有特殊含义.
    与圆括号不同的是花括号并不创建子shell,只是由当前的shell来读取并执行括起来的
    命令.有时用户希望使用一组命令的顺序输出作为另一组命令的输入,此时用花括号是很方
    便的.
    不论是用圆括号不是花括号,退出状态都是等于最后一条括起来的命令的退出状态.


    二,可以在当前shell中执行的命令

    用户在使用shell时一定要了解那些是可以在当前shell中执行的命令 那些不可以
    可以在当前shell中执行的命令有:

    break case cd continue
    echo eval exec exit
    export for if read
    readonly return set shift
    test times trap umask
    until wait while
    : {}

    posted @ 2008-03-14 14:03 随意门 阅读(202) | 评论 (0)编辑 收藏
    仅列出标题
    共9页: 1 2 3 4 5 6 7 8 9