精华内容
下载资源
问答
  • 编译过程在linux系统,使用gcc可以完成整个编译过程,gcc是什么? 它并不是一个编译器,而是一个驱动程序。编译过程中每个环节由具体的组件负责,编译过程由cc1负责、汇编过程由as汇编器负责、链接过程由ld负责。...

    编译过程

    在linux系统下,使用gcc可以完成整个编译过程,gcc是什么?
    它并不是一个编译器,而是一个驱动程序。编译过程中每个环节由具体的组件负责,编译过程由cc1负责、汇编过程由as汇编器负责、链接过程由ld负责。

    • 软件构建过程通常分为4个阶段:预编译(预处理)、编译、汇编、链接。

    预编译(预处理)

    我们在编译程序时可以通过加 -E选项告诉编译器仅作预处理
    gcc -E xxx.c -o xxx.i
    (1)文件包含
    文件包含命令指示预处理器将源文件的内容全部复制到当前源文件中。
    (2)宏定义
    宏可以提高程序的通用性和易读性,减少不一致性和输入错误,便于维护。
    在预处理过程中,预编器将宏名替换为具体的值。
    (3)条件编译
    在大多数情况下,源程序中所有的语句都参加编译,但有的时候用户希望按照一定的条件去编译源程序的不同部分,这时可以使用条件编译。

    编译

    编译程序对预处理过的结果进行词法分析、语法分析、语义分析,然后产生中间代码,并对中间代码进行优化,目标是使最终生成的可执行代码执行时间更短、占用的空间更小,最后生成相应的汇编代码。

    可以使用gcc -S xxx.c指定编译过程只进行编译,不进行汇编和链接。

    frame pointer和base pointer均指向栈帧的底部,在IA32(Intel Architecture 32bit)中,通常使用寄存器ebp保存这个位置。在汇编语言中,在函数的开头和结尾处分别会插入一小段代码,分别称为Prologue和Epilogue。

    Prologue保存主调函数的frame pointer,这是为了在子函数调用结束后,恢复主调函数的栈帧。同时为子函数准备栈帧。其主要操作包括:

    1.保存主调函数的frame pointer,将保存在寄存器ebp的frame pointer压栈。在退出子函数时可以从栈中恢复主调函数的frame pointer。
    2.将esp赋值给ebp,即将子函数的frame pointer指向主调函数的栈顶。
    3.修改栈顶指针esp,为子函数的本地变量分配栈空间。

    main函数并不是程序中第一个调用的函数,main函数也是一个被调函数,其也有栈帧。程序中第一个被调用的函数_start也会自己模拟一个栈帧。

    Epilogue功能与Prologue相反,其主要操作包括:

    1. 将栈指针esp指向当前子函数的栈帧的frame pointer,也就是说,指向当前栈帧的栈底,而这个位置,恰好是Prologue保存的主调函数的frame pointer。然后,通过指令pop将主调函数的frame pointer弹出到ebp中。
      2.将调用子函数时call指令压栈的返回地址从栈顶pop到EIP中,并跳转到EIP处继续执行。如此,CPU就返回到主调函数继续执行。

    汇编

    • 汇编器将汇编代码翻译为机器指令,汇编器的汇编过程相对编译器比较简单。汇编器的工作除了生成机器码外,汇编器还要在目标文件中创建辅助链接时需要的信息,包括符号表、重定位表。

    链接

    • 链接是编译过程的最后一个阶段,链接器将一个或者多个目标文件和库,包含动态库和静态库,链接为一个单独的文件。链接器的工作可以分为两个阶段:
      1. 第一阶段是将多个文件合并为一个单独的文件。对于可执行文件,还需要为指令及符号分配运行时地址。
      2. 进行符号重定位。

    如果在链接过程中有静态库,在链接静态库时,并不是将整个静态库中包含的目标文件全部复制一份到最终的可执行文件中,而是仅仅链接库中使用的目标文件。
    动态库在可执行文件中不会有任何副本
    1. 动态加载库需要知道可执行程序依赖的动态库,这样在加载可执行程序时才能加载其依赖的动态库。所以,在链接时,链接器将根据可执行程序引用的动态库中的符号的情况在dynamic段中记录可执行程序所依赖的动态库。
    2.链接器需要在重定位表中创建重定位记录,这样当动态链接器加载动态库时,将依据重定位记录动态库引用的这些外部符号。

    展开全文
  • Linux下C程序的编辑,编译和运行以及调试 要使用的工具: 编辑:vim(vi) 编译和运行:gcc(cc) 调试:gdb 安装很简单(以下是以在CentOS中安装为例): 1 yum vim gcc gdb  1.使用...

    LinuxC程序的编辑,编译和运行以及调试

    要使用的工具:
    编辑:vim(vi)
    编译和运行:gcc(cc)
    调试:gdb

    安装很简单(以下是以在CentOS中安装为例):

    1

    yum vim gcc gdb

     1.使用vim编辑源文件
    首先,打开终端练下手:

    1

    vim hello.c

    (进入一般模式)
    按下"i",进入编辑模式,在编辑模式下输入:

    1

    #include <stdio.h>

    2

    int main(){

     

    3

        printf("Hello, World!\n");

    4

        return 0;

     

    5

    }

     输入完成,按"ESC"键,回到一般模式,然后按下":wq",即可保存并退出vim

    附注:
    在一般模式下,按下":%!xxd"查看hello.c16进制形式,回到文本格式按下":%!xxd -r"
    查看hello.c的二进制形式,按下":%!xxd -b",这是hello.c保存在磁盘上的存储状态。
    至此,在vim已完成C源文件的编辑。
    关于vim的使用,直接上网搜索vim,相关的文章是相当多的;或者参考vim的联机帮助,在命令行上键入"man vim"即可。

    2.编译和运行

    gcc命令的基本用法:

    1

    gcc[options] [filenames]

     其中,filenames为文件名;options为编译选项
    当不使用任何编译选项编译hello.c时,gcc将会自动编译产生一个a.out的可执行文件:

    1

    [root@localhost c]# ls

    2

    hello.c

     

    3

    [root@localhost c]# gcc hello.c

    4

    [root@localhost c]# ls

     

    5

    a.out  hello.c

    执行:

    1

    [root@localhost c]# ./a.out

    2

    Hello, World!

    使用-o编译选择,可以为编译后的文件指定一个名字:

    1

    [root@localhost c]# ls

    2

    a.out  hello.c

     

    3

    [root@localhost c]# gcc hello.c -o hello

    4

    [root@localhost c]# ls

     

    5

    a.out  hello  hello.c

     执行:

    1

    [root@localhost c]# ./hello

    2

    Hello, World!

     注意:使用-o选项时,-o后面必须跟一个文件名,即:-o outfile

    为了便于描述后面的选项,删除helloa.out可执行文件。

    结合介绍gcc的编译选项,分析hello.c的编译和执行过程:
    1)预处理阶段:使用-E选项,对输入文件只做预处理不编译。当使用这个选项时,预处理器的输出被送到标准输出而不是存储到文件。如果想将预处理的输出存储到文件,可结合-o选项使用,使用如下:

    1

    [root@localhost c]# ls

    2

    hello.c

     

    3

    [root@localhost c]# gcc -E hello.c -o hello.i

    4

    [root@localhost c]# ls

     

    5

    hello.c  hello.i

     使用less查看下hello.i

    1

    [root@localhost c]# less hello.i

    2)编译阶段:使用-S选项,将C程序编译为汇编语言文件后停止编译,gcc编译产生汇编文件的默认后缀为.s

    1

    [root@localhost c]# ls

    2

    hello.c  hello.i

     

    3

    [root@localhost c]# gcc -S hello.c

    4

    [root@localhost c]# ls

     

    5

    hello.c  hello.i  hello.s

    gcc -S hello.c处,使用C源文件编译,也可以用gcc -S hello.i的预处理文件编译,结果一样。

    使用-S编译时,也可以和-o结合使用指定编译产生的汇编语言文件的名字:

    1

    [root@localhost c]# ls

    2

    hello.c  hello.i  hello.s

     

    3

    [root@localhost c]# gcc -S hello.i -o hello_s.s

    4

    [root@localhost c]# ls

     

    5

    hello.c  hello.i  hello.s  hello_s.s

    可使用less命令查看汇编代码。

    3)汇编阶段:使用-c选项,将C源文件或者汇编语言文件编译成可重定向的目标文件(二进制形式),其默认后缀为.o

    1

    [root@localhost c]# ls

    2

    hello.c  hello.i  hello.s  hello_s.s

     

    3

    [root@localhost c]# gcc -c hello.s

    4

    [root@localhost c]# ls

     

    5

    hello.c  hello.i  hello.o  hello.s  hello_s.s

    也可以和-o结合使用指定编译产生的目标文件的名字:

    1

    [root@localhost c]# gcc -c hello.s -o hello.o

    由于hello.o是二进制文件,使用less查看显示为乱码;

    然后使用vim hello.o打开也显示为乱码,按下":%!xxd"查看其16进制形式,按下":%!xxd -r"退出 16进制查看模式,回到乱码状态。在退出vim时,若提示已经修改了文件,则使用":q!"强制退出。

    4)链接阶段:链接器将可重定向的目标文件hello.o以及库文件(如printf.o)执行并入操作,形成最终可执行的可执行目标文件。

    1

    [root@localhost c]# ls

    2

    hello.c  hello.i  hello.o  hello.s  hello_s.s

     

    3

    [root@localhost c]# gcc hello.o

    4

    [root@localhost c]# ls

     

    5

    a.out  hello.c  hello.i  hello.o  hello.s  hello_s.s

     可使用-o选项,指定输出文件(即可执行目标文件)的名字:

    1

    [root@localhost c]# gcc hello.o -o hello

    2

    [root@localhost c]# ls

     

    3

    a.out  hello  hello.c  hello.i  hello.o  hello.s  hello_s.s

     5)执行阶段:

    1

    [root@localhost c]# ./a.out

    2

    Hello, World!

     

    3

    [root@localhost c]# ./hello

    4

    Hello, World!

     由此,看出前面使用的gcc hello.c -o hello命令,将hello.c直接编译为可执行的目标文件,中间经过于处理器的预处理阶段(源文件到预处理文件),编译器的编译阶段(预处理文件到汇编文件),汇编器的汇编阶段(汇编文件到可重定向的目标文件),链接器的链接阶段(可重定向的目标文件到可执行的目标文件)。

    还有其他的选项如下:
    -Idir
    dir是头文件所在的目录
    -Ldir
    dir是库文件所在的目录

    -Wall:打印所有的警告信息
    -Wl,options
    options是传递给链接器的选项

    编译优化选项:-O-O2
    -O
    选项告诉GCC 对源代码进行基本优化。这些优化在大多数情况下都会使程序执行的更快。-O2选项告诉GCC产生尽可能小和尽可能快的代码。
    -O2
    选项将使编译的速度比使用-O时慢。但通常产生的代码执行速度会更快。

    除了-O-O2优化选项外,还有一些低级选项用于产生更快的代码。这些选项非常的特殊,而且最好只有当你完全理解这些选项将会对编译后的代码产生什么样的效果时再去使用。这些选项的详细描述,请参考GCC的联机帮助,在命令行上键入"man gcc"即可。

    调试选项:-g(使用详情见第3部分)
    -g
    选项告诉GCC产生能被GNU调试器使用的调试信息以便调试你的程序。
    即:在生成的目标文件中添加调试信息,所谓调试信息就是源代码和指令之间的对应关系,在gdb调试和objdump反汇编时要用到这些信息。

    3.调试
    虽然GCC提供了调试选项,但是本身不能用于调试。Linux 提供了一个名为gdbGNU调试程序。gdb是一个用来调试CC++程序的调试器。它使你能在程序运行时观察程序的内部结构和内存的使用情况。以下是gdb所提供的一些功能
    a.
    它使你能监视你程序中变量的值;
    b.
    它使你能设置断点以使程序在指定的代码行上停止执行;
    c.
    它使你能一行行的执行你的代码。

    (1)启动gdb
    在命令行上键入"gdb"并按回车键就可以运行gdb了,如下:

    1

    [root@localhost c]# gdb

    2

    GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1)

     

    3

    Copyright (C) 2010 Free Software Foundation, Inc.

    4

    License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it.

     

    5

    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

    6

    and "show warranty" for details.

     

    7

    This GDB was configured as "x86_64-redhat-linux-gnu".

    8

    For bug reporting instructions, please see:<>.

     

    9

    (gdb)

    当启动gdb之后,即可在命令行上输入命令进行相关的调试操作。
    也可以以下面的方式来启动gdb:

    1

    [root@localhost c]# gdb hello

     这种方式启动gdb,直接将指定调试的程序文件装载到调试环境中。也就是让gdb装入名称为filename的可执行文件,从而准备调试。
    为了能够进行调试,当前调试的程序文件中必须包含调试信息。其中调试信息包含程序中的每个变量的类型和其在可执行文件里的地址映射以及源代码的行号,gdb利用这些信息使源代码和机器码相关联。因此在使用gcc编译源程序的时候必须使用-g选项,以便将调试信息包含在可执行文件中。
    例如:

    1

    [root@localhost c]# gcc -g hello.c -o hello

    gdb还提供了其他的启动选项,请参考gdb的联机帮助。在命令行上键入"man gdb"并回车即可。

    (2)gdb基本命令
    <1>
    单步执行和跟踪函数调用
    程序编辑如下:

    01

    #include <stdio.h>

    02

    int add_range(int low, int high){

     

    03

        int i;

    04

        int sum;

     

    05

        for(i = low; i <= high; i++){

    06

            sum = sum + i;

     

    07

        }

    08

        return sum;

     

    09

    }

    10

     

     

    11

    int main(){

    12

        int result[100];

     

    13

        result[0] = add_range(1, 10);

    14

        result[1] = add_range(1, 100);

     

    15

        printf("result[0] = %d\nresult[1] = %d\n", result[0], result[1]);

    16

        return 0;

     

    17

     

    18

    }

    编译和运行如下:

    1

    [root@localhost gdb_demo]# vim test1.c 

    2

    [root@localhost gdb_demo]# gcc test1.c -o test1

     

    3

    [root@localhost gdb_demo]# ls

    4

    test1  test1.c

     

    5

    [root@localhost gdb_demo]# ./test1

    6

    result[0] = 55

     

    7

    result[1] = 5105

     以上程序的结果中,显然第二个结果是不正确的,有基础的人会一眼看出问题处在哪里,呵呵,这里只是为了演示使用gdb调试而故意为之。当然在开发人员最好不要太过于依赖gdb才能找到错误。

    在编译时加上-g选项,生成的目标文件才能用gdb进行调试:

    01

    [root@localhost gdb_demo]# gcc test1.c -g -o test1

    02

    [root@localhost gdb_demo]# gdb test1

     

    03

    GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1)

    04

    Copyright (C) 2010 Free Software Foundation, Inc.

     

    05

    License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it.

    06

    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

     

    07

    and "show warranty" for details.

    08

    This GDB was configured as "x86_64-redhat-linux-gnu".

     

    09

    For bug reporting instructions, please see:<>...

    10

    Reading symbols from /root/code/c/gdb_demo/test1...done.

     

    11

    (gdb)

    -g选项的作用是在目标文件中加入源代码的信息,比如目标文件中的第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到目标文件中,所以在调试时目标文件必须保证gdb也能找到源文件。
    gdb
    提供一个类是shell的命令行环境,上面的(gdb)就是提示符,在这个提示符下输入help可以查看命令的类别:

    01

    (gdb) help

    02

    List of classes of commands:

     

    03

    aliases -- Aliases of other commands

    04

    breakpoints -- Making program stop at certain points

     

    05

    data -- Examining data

    06

    files -- Specifying and examining files

     

    07

    internals -- Maintenance commands

    08

    obscure -- Obscure features

     

    09

    running -- Running the program

    10

    stack -- Examining the stack

     

    11

    status -- Status inquiries

    12

    support -- Support facilities

     

    13

    tracepoints -- Tracing of program execution without stopping the program

    14

    user-defined -- User-defined commands

     

    15

    Type "help" followed by a class name for a list of commands in that class.

    16

    Type "help all" for the list of all commands.

     

    17

    Type "help" followed by command name for full documentation.

    18

    Type "apropos word" to search for commands related to "word".

     

    19

    Command name abbreviations are allowed if unambiguous.

     可以进一步查看某一个类别中有哪些命令,例如查看files类别下有哪些命令可以用:

    01

    (gdb) help files

    02

    Specifying and examining files.

     

    03

    List of commands:

    04

    add-symbol-file -- Load symbols from FILE

     

    05

    add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file

    06

    cd -- Set working directory to DIR for debugger and program being debugged

     

    07

    core-file -- Use FILE as core dump for examining memory and registers

    08

    directory -- Add directory DIR to beginning of search path for source files

     

    09

    edit -- Edit specified file or function

    10

    exec-file -- Use FILE as program for getting contents of pure memory

     

    11

    file -- Use FILE as program to be debugged

    12

    forward-search -- Search for regular expression (see regex(3)) from last line listed

     

    13

    generate-core-file -- Save a core file with the current state of the debugged process

    14

    list -- List specified function or line

     

    15

    load -- Dynamically load FILE into the running program

    使用list命令从第一行开始列出源代码:

    01

    (gdb) list 1

    02

    #include <stdio.h>

     

    03

    04

     int add_range(int low, int high){

     

    05

    int i;

    06

     int sum;

     

    07

    for(i = low; i <= high; i++){

    08

     sum = sum + i;

     

    09

    }

    10

      return sum;

     

    11

     }

    12

    (gdb)

    一次只列出10行,如果要从11行开始继续列出源代码可以输入:

    1

    (gdb) list

     也可以什么都不输入直接敲回车,gdb提供类一个方便的功能,在提示符下直接敲回车表示用适当的参数重复上一条命令。

    1

    (gdb) (直接回车)

    2

     

    3

    int main(){

    4

    int result[100];

     

    5

    result[0] = add_range(1, 10);

    6

    result[1] = add_range(1, 100);

     

    7

    printf("result[0] = %d\nresult[1] = %d\n", result[0], result[1]);

    8

    return 0;

     

    9

     }

    gdb的很多常用命令有简写形式,例如list命令可以写成l,要列出一个函数的源码也可以用函数名做list的参数:

    01

    (gdb) l add_range

    02

    #include <stdio.h>

     

    03

    04

     int add_range(int low, int high){

     

    05

    int i;

    06

    int sum;

     

    07

    for(i = low; i <= high; i++){

    08

     sum = sum + i;

     

    09

    }

    10

    return sum;

     

    11

    }

     现在退出gdb的环境(quit或简写形式q):

    1

    (gdb) quit

     现在把源代码改名或移动到别处,再用gdb调试目标文件,就列不出源代码了:

    01

    [root@localhost gdb_demo]# ls

    02

    test1  test1.c

     

    03

    [root@localhost gdb_demo]# mv test1.c test.c

    04

    [root@localhost gdb_demo]# ls

     

    05

    test1  test.c

    06

    [root@localhost gdb_demo]# gdb test1

     

    07

    ......

    08

    (gdb) l

     

    09

    5 test1.c: 没有那个文件或目录.

    10

     in test1.c

     

    11

    (gdb)

    可见gcc-g选项并不是把源代码嵌入到目标文件中的,在调试目标文件时也需要源文件。

    现在把源代码恢复原样,继续调试。首先使用start命令开始执行程序:

    01

    [root@localhost gdb_demo]# mv test.c test1.c

    02

    [root@localhost gdb_demo]# gdb test1

     

    03

    ......

    04

    (gdb) start

     

    05

    Temporary breakpoint 1 at 0x4004f8: file test1.c, line 14.

    06

    Starting program: /root/code/c/gdb_demo/test1

     

    07

    Temporary breakpoint 1, main () at test1.c:14

    08

    14     result[0] = add_range(1, 10);

     

    09

    Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64

    10

    (gdb)

    这表示停在main函数中变量定义之后的第一条语句处等待我们发命令,gdb列出这条语句表示它还没执行,并且马上要执行。我们可以用next命令(简写为n)控制这些语句一条一条地执行:

    1

    (gdb) n

    2

    15     result[1] = add_range(1, 100);

     

    3

    (gdb) (直接回车)

    4

    16     printf("result[0] = %d\nresult[1] = %d\n", result[0], result[1]);

     

    5

    (gdb) (直接回车)

    6

    result[0] = 55

     

    7

    result[1] = 5105

    8

    17     return 0;

    n命令依次执行两行赋值语句和一行打印语句,在执行打印语句时结果立刻打印出来类,然后停在return语句之前等待我们发命令。
    虽然我们完全控制了程序的执行,但仍然看不出哪里错了,因为错误不再main函数中而是在add_range函数中,现在用start命令重新执行,这次用step命令(简写为s)进入函数中去执行:

    01

    (gdb) start

    02

    The program being debugged has been started already.

     

    03

    Start it from the beginning? (y or n) y

    04

    Temporary breakpoint 3 at 0x4004f8: file test1.c, line 14.

     

    05

    Starting program: /root/code/c/gdb_demo/test1

    06

    Temporary breakpoint 3, main () at test1.c:14

     

    07

    14     result[0] = add_range(1, 10);

    08

    (gdb) s

     

    09

    add_range (low=1, high=10) at test1.c:6

    10

    6     for(i = low; i <= high; i++){

    这次进入add_range函数中,停在函数中定义变量之后的第一条语句处。
    在函数中有几种查看状态的办法,backtrace(简写为bt)可以查看函数调用的栈帧:

    1

    (gdb) bt

    2

    #0  add_range (low=1, high=10) at test1.c:6

     

    3

    #1  0x0000000000400507 in main () at test1.c:14

     可见当前的add_range函数是被main函数调用的,main函数中传给add_range的参数是low=1, high=10main函数的栈帧编号是1add_range函数的栈帧编号是0
    现在可以使用info命令(简写为i)查看add_range局部变量的值:

    1

    (gdb) i locals

    2

    i = 0

     

    3

    sum = 0

     如果想查看main函数中的当前局部变量的值也可以做到的,先用frame命令(简写为f)选择1号栈帧,然后再查看main中的局部变量:

    01

    (gdb) f 1

    02

    #1  0x0000000000400507 in main () at test1.c:14

     

    03

    14     result[0] = add_range(1, 10);

    04

    (gdb) i locals

     

    05

    result = {4195073, 0, -1646196904, 50, 4195016, 0, 0, 1, 2041, 1, -1654612390, 

    06

      50, 0, 0, -1652419360, 50, -7728, 32767, -7688, 32767, -1652420216, 50, 

     

    07

      -134227048, 32767, -163754450, 0, -1654612390, 50, 0, 0, -134227048, 32767, 

    08

      1, 0, 0, 0, 1, 50, -1652420216, 50, -7848, 32767, 750006344, 0, 6, 0, -7826, 

     

    09

      32767, 0, 0, -1652419360, 50, -7808, 32767, -1645672825, 50, -7784, 32767, 0, 

    10

      1, 0, 0, 4195073, 0, 191, 0, -7826, 32767, -7825, 32767, 1, 0, 0, 0, 

     

    11

      -1645672168, 50, 0, 0, 4195680, 0, 0, 0, 4195235, 0, -7512, 32767, 4195749, 

    12

      0, -1646199904, 50, 4195680, 0, 0, 0, 4195296, 0, -7536, 32767, 0, 0}

    注意到result数组中的很多元素具有杂乱无章的值,因为未经初始化的局部变量具有不确定的值。

    到目前为止(即已经进入第一次的函数调用的函数体内),是正常的。
    继续用sn往下走,然后用print(简写为p)打印变量sum的值。

    01

    (gdb) s

    02

    7         sum = sum + i;

     

    03

    (gdb) 

    04

    6     for(i = low; i <= high; i++){

     

    05

    (gdb) 

    06

    7         sum = sum + i;

     

    07

    (gdb) 

    08

    6     for(i = low; i <= high; i++){

     

    09

    (gdb) p sum

    10

    $1 = 3

     注意:这里的$1表示gdb保存着这些中间结果,$后面的编号会自动增长,在命令中可以用$1$2$3等编号代替相应的值。
    -----------------------------------------------------
    以上的执行过程使用下面的方法可能看得更清楚(这里的步骤不是继续跟着上面步骤,是在另一个终端中调试的):

    01

    (gdb) f 0

    02

    #0  add_range (low=1, high=10) at test1.c:7

     

    03

    7         sum = sum + i;

    04

    (gdb) i locals

     

    05

    i = 1

    06

    sum = 0

     

    07

    (gdb) s

    08

    6     for(i = low; i <= high; i++){

     

    09

    (gdb) i locals

    10

    i = 1

     

    11

    sum = 1

    12

    (gdb) s

     

    13

    7         sum = sum + i;

    14

    (gdb) i locals

     

    15

    i = 2

    16

    sum = 1

     

    17

    (gdb) s

    18

    6     for(i = low; i <= high; i++){

     

    19

    (gdb) i locals

    20

    i = 2

     

    21

    sum = 3

    由此看出,第一次循环的结果是正确的,再往下单步调试已经没有意义了,可以使用finish命令让程序一直运行到从当前函数返回为止:

    1

    (gdb) finish

    2

    Run till exit from #0  add_range (low=1, high=10) at test1.c:6

     

    3

    0x0000000000400507 in main () at test1.c:14

    4

    14     result[0] = add_range(1, 10);

     

    5

    Value returned is $1 = 55

    返回值是55,当前正准备执行赋值操作,用s命令执行赋值操作,然后查看result数组:

    01

    (gdb) s

    02

    15     result[1] = add_range(1, 100);

     

    03

    (gdb) print result

    04

    $2 = {55, 0, -1646196904, 50, 4195016, 0, 0, 1, 2041, 1, -1654612390, 50, 0, 0, 

     

    05

      -1652419360, 50, -7728, 32767, -7688, 32767, -1652420216, 50, -134227048, 

    06

      32767, -163754450, 0, -1654612390, 50, 0, 0, -134227048, 32767, 1, 0, 0, 0, 

     

    07

      1, 50, -1652420216, 50, -7848, 32767, 750006344, 0, 6, 0, -7826, 32767, 0, 0, 

    08

      -1652419360, 50, -7808, 32767, -1645672825, 50, -7784, 32767, 0, 1, 0, 0, 

     

    09

      4195073, 0, 191, 0, -7826, 32767, -7825, 32767, 1, 0, 0, 0, -1645672168, 50, 

    10

      0, 0, 4195680, 0, 0, 0, 4195235, 0, -7512, 32767, 4195749, 0, -1646199904, 

     

    11

      50, 4195680, 0, 0, 0, 4195296, 0, -7536, 32767, 0, 0}

    第一个值是55确实赋值给类result数组的第0个元素。

    使用s命令进入第二次add_range调用,进入之后首先查看参数和局部变量:

    1

    (gdb) s

    2

    add_range (low=1, high=100) at test1.c:6

     

    3

    6     for(i = low; i <= high; i++){

    4

    (gdb) bt

     

    5

    #0  add_range (low=1, high=100) at test1.c:6

    6

    #1  0x000000000040051c in main () at test1.c:15

     

    7

    (gdb) i locals

    8

    i = 11

     

    9

    sum = 55

     到这里,看出了问题:由于局部变量isum没有初始化,所以具有不确定的值,又由于连次调用是连续的,isum正好取类上次调用时的值。i的初始值不是0不要紧,因为在for循环开始重新赋值了,但如果sum的处置不是0,累加得到的结果就错了。
    问题找到了,可以退出gdb修改源代码了。然而我们不想浪费一次调试机会,可以在gdb中马上把sum的初始值改为0,继续运行,看看改了之后有没有其他的bug

    01

    (gdb) set var sum=0

    02

    (gdb) finish

     

    03

    Run till exit from #0  add_range (low=1, high=100) at test1.c:6

    04

    0x000000000040051c in main () at test1.c:15

     

    05

    15     result[1] = add_range(1, 100);

    06

    Value returned is $3 = 5050

     

    07

    (gdb) s

    08

    16     printf("result[0] = %d\nresult[1] = %d\n", result[0], result[1]);

     

    09

    (gdb) s

    10

    result[0] = 55

     

    11

    result[1] = 5050

    12

    17     return 0;

    这样结果就对了。
    修改变量的值除了用set命令之外也可以使用print命令,因为print命令后跟的是表达式,而我们知道赋值和函数调用都是表达式,所以还可以用print来修改变量的值,或者调用函数:

    1

    (gdb) print result[2]=88

    2

    $4 = 88

     

    3

    (gdb) p printf("result[2]=%d\n", result[2])

    4

    result[2]=88

     

    5

    $5 = 13

    我们知道:printf函数的返回值表示实际打印的字符数,所以$5的结果是13

    总结一下本节使用过的gdb命令:

    01

    list(l):列出源代码,接着上次的位置往下列,每次列10行

    02

    list 行号:列出产品从第几行开始的源代码

     

    03

    list 函数名:列出某个函数的源代码

    04

    start:开始执行程序,停在main函数第一行语句前面等待命令

     

    05

    next(n):执行下一列语句

    06

    step(s):执行下一行语句,如果有函数调用则进入到函数中

     

    07

    breaktrace(或bt):查看各级函数调用及参数

    08

    frame(f) 帧编号:选择栈帧

     

    09

    info(i) locals:查看当前栈帧局部变量的值

    10

    finish:执行到当前函数返回,然后挺下来等待命令

     

    11

    print(p):打印表达式的值,通过表达式可以修改变量的值或者调用函数

    12

    set var:修改变量的值

     

    13

    quit:退出gdb

     <2>断点
    程序编辑如下:

    01

    # include <stdio.h>

    02

    int main(){

     

    03

        int sum =0;

    04

        int i = 0;

     

    05

        char input[5];

    06

         

     

    07

        while(1){

    08

            scanf("%s", input);//在输入字符后自动加'\0'形成字符串

     

    09

            for(i = 0; input[i] != '\0'; i++){

    10

                sum = sum * 10 + input[i] - '0';//'1'-'0'=1,'\0'=0

     

    11

            }

    12

            printf("input = %d\n", sum);

     

    13

        }

    14

        return 0;

     

    15

    }

     编译和运行:

    01

    [root@localhost gdb_demo]# vim test2.c

    02

    [root@localhost gdb_demo]# gcc test2.c -g -o test2

     

    03

    [root@localhost gdb_demo]# ls

    04

    test1  test1.c  test2  test2.c

     

    05

    [root@localhost gdb_demo]# ./test2

    06

    123

     

    07

    input = 123

    08

    12345

     

    09

    input = 12345

    10

    12345678

     

    11

    input = -268647318

    12

    (Ctrl-C退出程序)

     从结果看出程序明显是有问题的。
    下面来调试:

    1

    [root@localhost gdb_demo]# gdb test2

    2

    ......

     

    3

    (gdb) start

    4

    Temporary breakpoint 1 at 0x40053c: file test2.c, line 4.

     

    5

    Starting program: /root/code/c/gdb_demo/test2

    6

    Temporary breakpoint 1, main () at test2.c:4

     

    7

    4     int sum =0;

    8

    Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64

     

    9

    (gdb)

    可见,start不会跳过赋值语句,通过第一次单步调试的例子,sum可列为重点观察对象,可以使用display命令使得每次停下来的时候都显示当前的sum,继续往下走:

    01

    (gdb) display sum

    02

    1: sum = 0

     

    03

    (gdb) n

    04

    5     int i = 0;

     

    05

    1: sum = 0

    06

    (gdb) 

     

    07

    9         scanf("%s", input);

    08

    1: sum = 0

     

    09

    (gdb) 

    10

    123

     

    11

    10         for(i = 0; input[i] != '\0'; i++){

    12

    1: sum = 0

     

    13

    (gdb)

    这个循环应该是没有问题的,因为循环开始sum的值是正确的。可以使用undisplay取消先前设置的那些变量的跟踪。

    如果不想一步一步跟踪这个循环,可以使用break(简写为b)在第8行设置一个断点(Breakpoint):

    01

    (gdb) l

    02

    5     int i = 0;

     

    03

    6     char input[5];

    04

    7     

     

    05

    8     while(1){

    06

    9         scanf("%s", input);

     

    07

    10         for(i = 0; input[i] != '\0'; i++){

    08

    11             sum = sum * 10 + input[i] - '0';

     

    09

    12         }

    10

    13         printf("input = %d\n", sum);

     

    11

    14     }

    12

    (gdb) b 8

     

    13

    Breakpoint 2 at 0x40054a: file test2.c, line 8.

     break命令的参数也可以是函数名,表示在某一个函数开头设置断点。现在用continue命令(简写为c)连续运行而非单步运行,程序到达断点会自动停下来,这样就可以停在下一次循环的开头:

    1

    (gdb) c

    2

    Continuing.

     

    3

    input = 123

    4

    Breakpoint 2, main () at test2.c:9

     

    5

    9         scanf("%s", input);

    6

    1: sum = 123

    然后输入新的字符串准备转换:

    1

    (gdb) n

    2

    12345

     

    3

    10         for(i = 0; input[i] != '\0'; i++){

    4

    1: sum = 123

    此时问题已经暴露出来了,新的转换应该是从0开始累加的,而现在sum却是123,原因在于新的循环没有把sum归零。

    可见断点有助于快速跳过与问题无关的代码,然后在有问题的代码上慢慢走慢慢分析,“断点加单步是使用调试器的基本方法。至于应该在哪里设置断点,怎么知道哪些代码可以跳过而哪些代码要慢慢走,也要通过对错误现象的分析和假设来确定。

    一次调试可以设置多个断点,用info命令(简写为i)可以查看已经设置的断点:

    1

    (gdb) b 11

    2

    Breakpoint 3 at 0x40056c: file test2.c, line 11.

     

    3

    (gdb) i breakpoints

    4

    Num     Type           Disp Enb Address            What

     

    5

    2       breakpoint     keep y   0x000000000040054a in main at test2.c:8

    6

     breakpoint already hit 2 times

     

    7

    3       breakpoint     keep y   0x000000000040056c in main at test2.c:11

     

    每一个断点都有一个编号,可以用编号指定删除某个断点:

    1

    (gdb) delete breakpoints 2

    2

    (gdb) i breakpoints

     

    3

    Num     Type           Disp Enb Address            What

    4

    3       breakpoint     keep y   0x000000000040056c in main at test2.c:11

     有时候一个断点不想用可以禁用而不必删除,这样以后想用的时候可以直接启用,而不必重新从代码里找应该在哪一行设置断点:

    01

    (gdb) disable breakpoints 3

    02

    (gdb) i breakpoints

     

    03

    Num     Type           Disp Enb Address            What

    04

    3       breakpoint     keep n   0x000000000040056c in main at test2.c:11

     

    05

    (gdb) enable breakpoints 3

    06

    (gdb) i breakpoints

     

    07

    Num     Type           Disp Enb Address            What

    08

    3       breakpoint     keep y   0x000000000040056c in main at test2.c:11

     

    09

    (gdb) delete breakpoints

    10

    Delete all breakpoints? (y or n) y

     

    11

    (gdb) i breakpoints

    12

    No breakpoints or watchpoints.

    gdb设置断点功能非常灵活,还可以设置断点在满足某个条件时才激活,例如我们仍然在循环开头设置断点,但是仅当sum不等于0时才中断,然后用run(简写为r)重新从程序开头连续执行:

    01

    (gdb) break 10 if sum != 0

    02

    Breakpoint 5 at 0x400563: file test2.c, line 10.

     

    03

    (gdb) i breakpoints

    04

    Num     Type           Disp Enb Address            What

     

    05

    5       breakpoint     keep y   0x0000000000400563 in main at test2.c:10

    06

     stop only if sum != 0

     

    07

    (gdb) r

    08

    The program being debugged has been started already.

     

    09

    Start it from the beginning? (y or n) y

    10

    Starting program: /root/code/c/gdb_demo/test2 

     

    11

    123

    12

    input = 123

     

    13

    123

    14

    Breakpoint 5, main () at test2.c:10

     

    15

    10         for(i = 0; input[i] != '\0'; i++){

    16

    1: sum = 123

    结果:第一次执行输入之后没有中断,第二次却中断了,因为第二次循环开始sum的值为123

    总结一下本节使用到的gdb命令:

    01

    break(b) 行号:在某一行设置断点

    02

    break 函数名:在某个函数开头设置断点

     

    03

    break...if...:设置条件断点

    04

    continue(或c):从当前位置开始连续而非单步执行程序

     

    05

    delete breakpoints:删除所有断点

    06

    delete breakpoints n:删除序号为n的断点

     

    07

    disable breakpoints:禁用断点

    08

    enable breakpoints:启用断点

     

    09

    info(或i) breakpoints:参看当前设置了哪些断点

    10

    run(或r):从开始连续而非单步执行程序

     

    11

    display 变量名:跟踪查看一个变量,每次停下来都显示它的值

    12

    undisplay:取消对先前设置的那些变量的跟踪

     下面再看一个例子:
    程序如下:

    01

    #include <stdio.h>

    02

    int main(){

     

    03

        int i;

    04

        char str[6] = "hello";

     

    05

        char reverse_str[6] = "";

    06

        printf("%s\n", str);

     

    07

        for(i = 0; i < 5; i ++){

    08

            reverse_str[5-i] = str[i];

     

    09

        }

    10

        printf("%s\n", reverse_str);

     

    11

        return 0;

    12

    }

     运行结果:

    1

    [root@localhost gdb_demo]# gcc test3.c -g -o test3

    2

    [root@localhost gdb_demo]# ./test3

     

    3

    hello

    其中,第二次打印空白,结果显然是不正确的。
    调试过程如下:

    01

    [root@localhost gdb_demo]# gdb test3

    02

    ......

     

    03

    (gdb) l

    04

    1 #include <stdio.h>

     

    05

    06

    3 int main(){

     

    07

    4     int i;

    08

    5     char str[6] = "hello";

     

    09

    6     char reverse_str[6] = "";

    10

     

    11

    8     printf("%s\n", str);

    12

    9     for(i = 0; i < 5; i ++){

     

    13

    10         reverse_str[5-i] = str[i];

    14

    (gdb) 

     

    15

    11     }

    16

    12     printf("%s\n", reverse_str);

     

    17

    13     return 0;

    18

    14 }

     

    19

    (gdb) i breakpoints

    20

    No breakpoints or watchpoints.

     

    21

    (gdb) b 10

    22

    Breakpoint 1 at 0x4004fb: file test3.c, line 10.

     

    23

    (gdb) i breakpoints

    24

    Num     Type           Disp Enb Address            What

     

    25

    1       breakpoint     keep y   0x00000000004004fb in main at test3.c:10

    26

    (gdb) r

     

    27

    Starting program: /root/code/c/gdb_demo/test3 

    28

    hello

     

    29

    Breakpoint 1, main () at test3.c:10

    30

    10         reverse_str[5-i] = str[i];

     

    31

    Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64

    32

    (gdb) p reverse_str

     

    33

    $1 = "\000\000\000\000\000"

    34

    (gdb) c

     

    35

    Continuing.

    36

    Breakpoint 1, main () at test3.c:10

     

    37

    10         reverse_str[5-i] = str[i];

    38

    (gdb) p reverse_str

     

    39

    $2 = "\000\000\000\000\000h"

    40

    (gdb) c

     

    41

    Continuing.

    42

    Breakpoint 1, main () at test3.c:10

     

    43

    10         reverse_str[5-i] = str[i];

    44

    (gdb) p reverse_str

     

    45

    $3 = "\000\000\000\000eh"

    46

    (gdb) c

     

    47

    Continuing.

    48

    Breakpoint 1, main () at test3.c:10

     

    49

    10         reverse_str[5-i] = str[i];

    50

    (gdb) p reverse_str

     

    51

    $4 = "\000\000\000leh"

    52

    (gdb) c

     

    53

    Continuing.

    54

    Breakpoint 1, main () at test3.c:10

     

    55

    10         reverse_str[5-i] = str[i];

    56

    (gdb) p reverse_str

     

    57

    $5 = "\000\000lleh"

    58

    (gdb) c

     

    59

    Continuing.

    60

    Program exited normally.

     

    61

    (gdb)

     由上面的观察可知:
    在于将str数组中的值赋值到reverse_str的时候,将str的第1个元素赋值给类reverse_str的第6个元素,而该循环只循环了5次(即str数组元素个数),从而导致reverse_str的第一个元素为'\000',所以输出为空白。
    修改如下:

    01

    #include <stdio.h>

    02

    #include <string.h>

     

    03

    int main(){

    04

        int i;

     

    05

        char str[6] = "hello";

    06

        char reverse_str[6] = "";

     

    07

        printf("%s\n", str);

    08

        int len = strlen(str);

     

    09

        for(i = 0; i <= len-1; i ++){

    10

            reverse_str[len-1-i] = str[i];

     

    11

        }

    12

        printf("%s\n", reverse_str);

     

    13

        return 0;

    14

    }

     再次运行就好了:

    1

    [root@localhost gdb_demo]# gcc test3.c -o test3

    2

    [root@localhost gdb_demo]# ./test3

     

    3

    hello

    4

    olleh

    <3>观察点(Watchpoint)
    断点是当程序执行到某一代码行时中断,而观察点一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。
    vim test4.c

    01

    # include <stdio.h>

    02

    int main(){

     

    03

        int sum =0;

    04

        int i;

     

    05

        for(i = 1; i <= 10; i++){

    06

            sum = sum +i;

     

    07

        }

    08

        printf("sum = %d\n", sum);

     

    09

        return 0;

    10

    }

    编译运行:

    1

    [root@localhost gdb_demo]# gcc test4.c -g -o test4

    2

    [root@localhost gdb_demo]# ./test4

     

    3

    sum = 55

    设置观察点进行调试:

    01

    [root@localhost gdb_demo]# gdb test4

    02

    ......

     

    03

    (gdb) start

    04

    Temporary breakpoint 1 at 0x4004cc: file test4.c, line 4.

     

    05

    Starting program: /root/code/c/gdb_demo/test4

    06

    Temporary breakpoint 1, main () at test4.c:4

     

    07

    4     int sum =0;

    08

    Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64

     

    09

    (gdb) l

    10

    1 # include <stdio.h>

     

    11

    12

    3 int main(){

     

    13

    4     int sum =0;

    14

    5     int i;

     

    15

    6     for(i = 1; i <= 10; i++){

    16

    7         sum = sum +i;

     

    17

    8     }

    18

    9     printf("sum = %d\n", sum);

     

    19

    10     return 0;

    20

    (gdb) 

     

    21

    11 }

    22

    (gdb) watch sum

     

    23

    Hardware watchpoint 2: sum

    24

    (gdb) c

     

    25

    Continuing.

    26

    Hardware watchpoint 2: sum

     

    27

    Old value = 0

    28

    New value = 1

     

    29

    main () at test4.c:6

    30

    6     for(i = 1; i <= 10; i++){

     

    31

    (gdb) c

    32

    Continuing.

     

    33

    Hardware watchpoint 2: sum

    34

    Old value = 1

     

    35

    New value = 3

    36

    main () at test4.c:6

     

    37

    6     for(i = 1; i <= 10; i++){

    38

    (gdb) c

     

    39

    Continuing.

    40

    Hardware watchpoint 2: sum

     

    41

    Old value = 3

    42

    New value = 6

     

    43

    main () at test4.c:6

    44

    6     for(i = 1; i <= 10; i++){

     

    45

    (gdb) c

    46

    Continuing.

     

    47

    Hardware watchpoint 2: sum

    48

    Old value = 6

     

    49

    New value = 10

    50

    main () at test4.c:6

     

    51

    6     for(i = 1; i <= 10; i++){

    52

    (gdb)

    总结一下本节使用到的gdb命令:

    1

    watch:设置观察点

    2

    info(或i) watchpoints:查看当前设置了哪些观察点

    GDB的补充:

    输出格式:
    一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。
    例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要
    做到这样,你可以使用GDB的数据显示格式: 
    x
    按十六进制格式显示变量。 
    d
    按十进制格式显示变量。 
    u
    按十六进制格式显示无符号整型。 
    o
    按八进制格式显示变量。 
    t
    按二进制格式显示变量。 
    a
    按十六进制格式显示变量。 
    c
    按字符格式显示变量。 
    f
    按浮点数格式显示变量。

    01

    (gdb) p sum

    02

    $1 = 10

     

    03

    (gdb) p/a sum

    04

    $2 = 0xa

     

    05

    (gdb) p/x sum

    06

    $3 = 0xa

     

    07

    (gdb) p/o sum

    08

    $4 = 012

     

    09

    (gdb) p/t sum

    10

    $5 = 1010

     

    11

    (gdb) p/f sum

    12

    $6 = 1.40129846e-44

     

    13

    (gdb) p/c sum

    14

    $7 = 10 '\n'

    查看内存:
    你可以使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示: 
    x/ 
    n
    fu是可选的参数。 
    n
    是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。 
    f
    表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i
    u
    表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4bytesu参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作
    一个值取出来。 
    表示一个内存地址。 
    n/f/u
    三个参数可以一起使用。例如: 
    命令:x/3uh 0x54320 表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。

    展开全文
  • Linux下编写程序必要工具使用详解

    千次阅读 2016-10-08 22:44:39
    声明:本文转载自博客园:GCC 编译详解 - azraelly - 博客园 ...GNU CC(简称为Gcc)是GNU项目中符合ANSI C标准的编译系统,能够编译用C、C++和Object C等语言编写的程序。Gcc不仅

    声明:本文转载自博客园:GCC 编译详解 - azraelly - 博客园  http://www.cnblogs.com/azraelly/archive/2012/07/07/2580839.html


    GCC编译详解

    GNU CC(简称为Gcc)是GNU项目中符合ANSI C标准的编译系统,能够编译用C、C++和Object C等语言编写的程序。Gcc不仅功能强大,而且可以编译如C、C++、Object C、Java、Fortran、Pascal、Modula-3和Ada等多种语言,而且Gcc又是一个交叉平台编译器,它能够在当前CPU平台上为多种不同体系结构的硬件平台开发软件,因此尤其适合在嵌入式领域的开发编译。本章中的示例,除非特别注明,否则均采用Gcc版本为4.0.0。

    GCC入门基础

    表3.6 Gcc所支持后缀名解释

    后 缀 名

    所对应的语言

    后 缀 名

    所对应的语言

    .c

    C原始程序

    .s/.S

    汇编语言原始程序

    .C/.cc/.cxx

    C++原始程序

    .h

    预处理文件(头文件)

    .m

    Objective-C原始程序

    .o

    目标文件

    .i

    已经过预处理的C原始程序

    .a/.so

    编译后的库文件

    .ii

    已经过预处理的C++原始程序

       

    如本章开头提到的,Gcc的编译流程分为了四个步骤,分别为:

    · 预处理(Pre-Processing)

    · 编译(Compiling)

    · 汇编(Assembling)

    · 链接(Linking)

    下面就具体来查看一下Gcc是如何完成四个步骤的。

    首先,有以下hello.c源代码

    #include<stdio.h>
    
    int main()
    
    {
    
    printf("Hello! This is our embedded world!n");
    
    return 0;
    
    }

    (1)预处理阶段

    在该阶段,编译器将上述代码中的stdio.h编译进来,并且用户可以使用Gcc的选项”-E”进行查看,该选项的作用是让Gcc在预处理结束后停止编译过程。

     

    注意

    Gcc指令的一般格式为:Gcc [选项] 要编译的文件 [选项] [目标文件]

    其中,目标文件可缺省,Gcc默认生成可执行的文件,命为:编译文件.out

     

    [root@localhost Gcc]# Gcc –E hello.c –o hello.i

     

    在此处,选项”-o”是指目标文件,由表3.6可知,”.i”文件为已经过预处理的C原始程序。以下列出了hello.i文件的部分内容:

     

    typedef int (*__gconv_trans_fct) (struct __gconv_step *,
    
    struct __gconv_step_data *, void *,
    
    __const unsigned char *,
    
    __const unsigned char **,
    
    __const unsigned char *, unsigned char **,
    
    size_t *);
    
    …
    
    # 2 "hello.c" 2
    
    int main()
    
    {
    
    printf("Hello! This is our embedded world!n");
    
    return 0;
    
    }

    由此可见,Gcc确实进行了预处理,它把”stdio.h”的内容插入到hello.i文件中。

    (2)编译阶段

    接下来进行的是编译阶段,在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。

     

    [root@localhost Gcc]# Gcc –S hello.i –o hello.s

     

    以下列出了hello.s的内容,可见Gcc已经将其转化为汇编了,感兴趣的读者可以分析一下这一行简单的C语言小程序是如何用汇编代码实现的。

     

    .file "hello.c"
    
    .section .rodata
    
    .align 4
    
    .LC0:
    
    .string"Hello! This is our embedded world!"
    
    .text
    
    .globl main
    
    .type main, @function
    
    main:
    
    pushl �p
    
    movl %esp, �p
    
    subl $8, %esp
    
    andl $-16, %esp
    
    movl $0, �x
    
    addl $15, �x
    
    addl $15, �x
    
    shrl $4, �x
    
    sall $4, �x
    
    subl �x, %esp
    
    subl $12, %esp
    
    pushl $.LC0
    
    call puts
    
    addl $16, %esp
    
    movl $0, �x
    
    leave
    
    ret
    
    .size main, .-main
    
    .ident "GCC: (GNU) 4.0.0 20050519 (Red Hat 4.0.0-8)"
    
    .section .note.GNU-stack,"",@progbits
    
     

    (3)汇编阶段

    汇编阶段是把编译阶段生成的”.s”文件转成目标文件,读者在此可使用选项”-c”就可看到汇编代码已转化为”.o”的二进制目标代码了。如下所示:

     

    [root@localhost Gcc]# Gcc –c hello.s –o hello.o

     

    (4)链接阶段

    在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。

    读者可以重新查看这个小程序,在这个程序中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”函数的呢?最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,Gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf”了,而这也就是链接的作用。

    函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。Gcc在编译时默认使用动态库。

    完成了链接之后,Gcc就可以生成可执行文件,如下所示。

     

    [root@localhost Gcc]# Gcc hello.o –o hello

     

    运行该可执行文件,出现正确的结果如下。

     

    [root@localhost Gcc]# ./hello

    Hello! This is our embedded world!

    Gcc编译选项分析

    Gcc有超过100个的可用选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项。以下对每一类中最常用的选项进行讲解。

    (1)总体选项

    Gcc的总结选项如表3.7所示,很多在前面的示例中已经有所涉及。

    表3.7 Gcc总体选项列表

    后缀名

    所对应的语言

    -c

    只是编译不链接,生成目标文件“.o”

    -S

    只是编译不汇编,生成汇编代码

    -E

    只进行预编译,不做其他处理

    -g

    在可执行程序中包含标准调试信息

    -o file

    把输出文件输出到file里

    -v

    打印出编译器内部编译各过程的命令行信息和编译器的版本

    -I dir

    在头文件的搜索路径列表中添加dir目录

    -L dir

    在库文件的搜索路径列表中添加dir目录

    -static

    链接静态库

    -llibrary

    连接名为library的库文件

     

    对于“-c”、“-E”、“-o”、“-S”选项在前一小节中已经讲解了其使用方法,在此主要讲解另外两个非常常用的库依赖选项“-I dir”和“-L dir”。

    · “-I dir”

    正如上表中所述,“-I dir”选项可以在头文件的搜索路径列表中添加dir目录。由于Linux中头文件都默认放到了“/usr/include/”目录下,因此,当用户希望添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,Gcc就会到相应的位置查找对应的目录。

    比如在“/root/workplace/Gcc”下有两个文件:

     

    #include<my.h>
    
    int main()
    
    {
    
    printf(“Hello!!n”);
    
    return 0;
    
    }
    
     
    
    #include<stdio.h>

    这样,就可在Gcc命令行中加入“-I”选项:

     

    [root@localhost Gcc] Gcc hello1.c –I /root/workplace/Gcc/ -o hello1

     

    这样,Gcc就能够执行出正确结果。

     

    小知识

    在include语句中,“<>”表示在标准路径中搜索头文件,““””表示在本目录中搜索。故在上例中,可把hello1.c的“#include<my.h>”改为“#include “my.h””,就不需要加上“-I”选项了。

     

    · “-L dir”

    选项“-L dir”的功能与“-I dir”类似,能够在库文件的搜索路径列表中添加dir目录。例如有程序hello_sq.c需要用到目录“/root/workplace/Gcc/lib”下的一个动态库libsunq.so,则只需键入如下命令即可:

     

    [root@localhost Gcc] Gcc hello_sq.c –L /root/workplace/Gcc/lib –lsunq –o hello_sq

     

    需要注意的是,“-I dir”和“-L dir”都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。

    另外值得详细解释一下的是“-l”选项,它指示Gcc去连接库文件libsunq.so。由于在Linux下的库文件命名时有一个规定:必须以lib三个字母开头。因此在用-l选项指定链接的库文件名时可以省去lib三个字母。也就是说Gcc在对”-lsunq”进行处理时,会自动去链接名为libsunq.so的文件。

    (2)告警和出错选项

    Gcc的告警和出错选项如表3.8所示。

    表3.8 Gcc总体选项列表

    选项

    含义

    -ansi

    支持符合ANSI标准的C程序

    -pedantic

    允许发出ANSI C标准所列的全部警告信息

    选项

    含义

    -pedantic-error

    允许发出ANSI C标准所列的全部错误信息

    -w

    关闭所有告警

    -Wall

    允许发出Gcc提供的所有有用的报警信息

    -werror

    把所有的告警信息转化为错误信息,并在告警发生时终止编译过程

     

    下面结合实例对这几个告警和出错选项进行简单的讲解。

    如有以下程序段:

     

    #include<stdio.h>
    
     
    
    void main()
    
    {
    
    long long tmp = 1;
    
    printf(“This is a bad code!n”);
    
    return 0;
    
    }

    这是一个很糟糕的程序,读者可以考虑一下有哪些问题?

    · “-ansi”

    该选项强制Gcc生成标准语法所要求的告警信息,尽管这还并不能保证所有没有警告的程序都是符合ANSI C标准的。运行结果如下所示:

     

    [root@localhost Gcc]# Gcc –ansi warning.c –o warning

    warning.c: 在函数“main”中:

    warning.c:7 警告:在无返回值的函数中,“return”带返回值

    warning.c:4 警告:“main”的返回类型不是“int”

     

    可以看出,该选项并没有发现”long long”这个无效数据类型的错误。

    · “-pedantic”

    允许发出ANSI C标准所列的全部警告信息,同样也保证所有没有警告的程序都是符合ANSI C标准的。其运行结果如下所示:

     

    [root@localhost Gcc]# Gcc –pedantic warning.c –o warning

    warning.c: 在函数“main”中:

    warning.c:5 警告:ISO C90不支持“long long”

    warning.c:7 警告:在无返回值的函数中,“return”带返回值

    warning.c:4 警告:“main”的返回类型不是“int”

     

    可以看出,使用该选项查看出了”long long”这个无效数据类型的错误。

    · “-Wall”

    允许发出Gcc能够提供的所有有用的报警信息。该选项的运行结果如下所示:

    [root@localhost Gcc]# Gcc –Wall warning.c –o warning

    warning.c:4 警告:“main”的返回类型不是“int”

    warning.c: 在函数”main”中:

    warning.c:7 警告:在无返回值的函数中,”return”带返回值

    warning.c:5 警告:未使用的变量“tmp”

     

    使用“-Wall”选项找出了未使用的变量tmp,但它并没有找出无效数据类型的错误。

    另外,Gcc还可以利用选项对单独的常见错误分别指定警告,有关具体选项的含义感兴趣的读者可以查看Gcc手册进行学习。

    (3)优化选项

    Gcc可以对代码进行优化,它通过编译选项“-On”来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的Gcc来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。

    不同的优化级别对应不同的优化处理工作。如使用优化选项“-O”主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项“-O2”除了完成所有“-O1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项“-O3”则还包括循环展开和其他一些与处理器特性相关的优化工作。

    虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦。所以笔者建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。

    (4)体系结构相关选项

    Gcc的体系结构相关选项如表3.9所示。

    表3.9Gcc体系结构相关选项列表

    选项

    含义

    -mcpu=type

    针对不同的CPU使用相应的CPU指令。可选择的type有i386、i486、pentium及i686等

    -mieee-fp

    使用IEEE标准进行浮点数的比较

    -mno-ieee-fp

    不使用IEEE标准进行浮点数的比较

    -msoft-float

    输出包含浮点库调用的目标代码

    -mshort

    把int类型作为16位处理,相当于short int

    -mrtd

    强行将函数参数个数固定的函数用ret NUM返回,节省调用函数的一条指令

     

    这些体系结构相关选项在嵌入式的设计中会有较多的应用,读者需根据不同体系结构将对应的选项进行组合处理。在本书后面涉及到具体实例会有针对性的讲解。

    Gdb调试器

    调试是所有程序员都会面临的问题。如何提高程序员的调试效率,更好更快地定位程序中的问题从而加快程序开发的进度,是大家共同面对的。就如读者熟知的Windows下的一些调试工具,如VC自带的如设置断点、单步跟踪等,都受到了广大用户的赞赏。那么,在Linux下有什么很好的调试工具呢?

    本文所介绍的Gdb调试器是一款GNU开发组织并发布的UNIX/Linux下的程序调试工具。虽然,它没有图形化的友好界面,但是它强大的功能也足以与微软的VC工具等媲美。下面就请跟随笔者一步步学习Gdb调试器。

    Gdb使用流程

    首先,笔者给出了一个短小的程序,由此带领读者熟悉一下Gdb的使用流程。强烈建议读者能够实际动手操作。

    首先,打开Linux下的编辑器Vi或者Emacs,编辑如下代码。(由于为了更好地熟悉Gdb的操作,笔者在此使用Vi编辑,希望读者能够参见3.3节中对Vi的介绍,并熟练使用Vi)。

     

    #include <stdio.h>
    
    int sum(int m);
    
    int main()
    
    {
    
    int i,n=0;
    
    sum(50);
    
    for(i=1; i<=50; i++)
    
    {
    
    n += i;
    
    }
    
    printf("The sum of 1-50 is %d n", n );
    
     
    
    }
    
    int sum(int m)
    
    {
    
    int i,n=0;
    
    for(i=1; i<=m;i++)
    
    n += i;
    
    printf("The sum of 1-m is %dn", n);
    
    }
    
     

    在保存退出后首先使用Gcc对test.c进行编译,注意一定要加上选项”-g”,这样编译出的可执行代码中才包含调试信息,否则之后Gdb无法载入该可执行文件。

     

    [root@localhost Gdb]# gcc -g test.c -o test

     

    虽然这段程序没有错误,但调试完全正确的程序可以更加了解Gdb的使用流程。接下来就启动Gdb进行调试。注意,Gdb进行调试的是可执行文件,而不是如”.c”的源代码,因此,需要先通过Gcc编译生成可执行文件才能用Gdb进行调试。

     

    [root@localhost Gdb]# gdb test

    GNU Gdb Red Hat Linux (6.3.0.0-1.21rh)

    Copyright 2004 Free Software Foundation, Inc.

    GDB is free software, covered by the GNU General Public License, and you are

    welcome to change it and/or distribute copies of it under certain conditions.

    Type "show copying" to see the conditions.

    There is absolutely no warranty for GDB. Type "show warranty" for details.

    This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/lib/libthread_db.so.1".

    (gdb)

     

    可以看出,在Gdb的启动画面中指出了Gdb的版本号、使用的库文件等信息,接下来就进入了由“(gdb)”开头的命令行界面了。

    (1)查看文件

    在Gdb中键入”l”(list)就可以查看所载入的文件,如下所示:

     

     

    注意

    在Gdb的命令中都可使用缩略形式的命令,如“l”代便“list”,“b”代表“breakpoint”,“p”代表“print”等,读者也可使用“help”命令查看帮助信息。

     

    (Gdb) l

    1 #include <stdio.h>

    2 int sum(int m);

    3 int main()

    4 {

    5 int i,n=0;

    6 sum(50);

    7 for(i=1; i<=50; i++)

    8 {

    9 n += i;

    10 }

    (Gdb) l

    11 printf("The sum of 1~50 is %d n", n );

    12

    13 }

    14 int sum(int m)

    15 {

    16 int i,n=0;

    17 for(i=1; i<=m;i++)

    18 n += i;

    19 printf("The sum of 1~m is = %dn", n);

    20 }

     

    可以看出,Gdb列出的源代码中明确地给出了对应的行号,这样就可以大大地方便代码的定位。

    (2)设置断点

    设置断点是调试程序中是一个非常重要的手段,它可以使程序到一定位置暂停它的运行。因此,程序员在该位置处可以方便地查看变量的值、堆栈情况等,从而找出代码的症结所在。

    在Gdb中设置断点非常简单,只需在”b”后加入对应的行号即可(这是最常用的方式,另外还有其他方式设置断点)。如下所示:

     

    (Gdb) b 6

    Breakpoint 1 at 0x804846d: file test.c, line 6.

     

    要注意的是,在Gdb中利用行号设置断点是指代码运行到对应行之前将其停止,如上例中,代码运行到第五行之前暂停(并没有运行第五行)。

    (3)查看断点情况

    在设置完断点之后,用户可以键入”info b”来查看设置断点情况,在Gdb中可以设置多个断点。

     

    (Gdb) info b

    Num Type Disp Enb Address What

    1 breakpoint keep y 0x0804846d in main at test.c:6

     

    (4)运行代码

    接下来就可运行代码了,Gdb默认从首行开始运行代码,可键入”r”(run)即可(若想从程序中指定行开始运行,可在r后面加上行号)。

     

    (Gdb) r

    Starting program: /root/workplace/Gdb/test

    Reading symbols from shared object read from target memory...done.

    Loaded system supplied DSO at 0x5fb000

     

    Breakpoint 1, main () at test.c:6

    6 sum(50);

     

    可以看到,程序运行到断点处就停止了。

    (5)查看变量值

    在程序停止运行之后,程序员所要做的工作是查看断点处的相关变量值。在Gdb中只需键入”p”+变量值即可,如下所示:

     

    (Gdb) p n

    $1 = 0

    (Gdb) p i

    $2 = 134518440

     

    在此处,为什么变量”i”的值为如此奇怪的一个数字呢?原因就在于程序是在断点设置的对应行之前停止的,那么在此时,并没有把”i”的数值赋为零,而只是一个随机的数字。但变量”n”是在第四行赋值的,故在此时已经为零。

     

    小技巧

    Gdb在显示变量值时都会在对应值之前加上”$N”标记,它是当前变量值的引用标记,所以以后若想再次引用此变量就可以直接写作”$N”,而无需写冗长的变量名。

     

    (6)单步运行

    单步运行可以使用命令”n”(next)或”s”(step),它们之间的区别在于:若有函数调用的时候,”s”会进入该函数而”n”不会进入该函数。因此,”s”就类似于VC等工具中的”step in”,”n”类似与VC等工具中的”step over”。它们的使用如下所示:

     

    (Gdb) n

    The sum of 1-m is 1275

    7 for(i=1; i<=50; i++)

    (Gdb) s

    sum (m=50) at test.c:16

    16 int i,n=0;

     

    可见,使用”n”后,程序显示函数sum的运行结果并向下执行,而使用”s”后则进入到sum函数之中单步运行。

    (7)恢复程序运行

    在查看完所需变量及堆栈情况后,就可以使用命令”c”(continue)恢复程序的正常运行了。这时,它会把剩余还未执行的程序执行完,并显示剩余程序中的执行结果。以下是之前使用”n”命令恢复后的执行结果:

     

    (Gdb) c

    Continuing.

    The sum of 1-50 is :1275

     

    Program exited with code 031.

     

    可以看出,程序在运行完后退出,之后程序处于“停止状态”。

     

    小知识

    在Gdb中,程序的运行状态有“运行”、“暂停”和“停止”三种,其中“暂停”状态为程序遇到了断点或观察点之类的,程序暂时停止运行,而此时函数的地址、函数参数、函数内的局部变量都会被压入“栈”(Stack)中。故在这种状态下可以查看函数的变量值等各种属性。但在函数处于“停止”状态之后,“栈”就会自动撤销,它也就无法查看各种信息了。

    Gdb基本命令

    Gdb的命令可以通过查看help进行查找,由于Gdb的命令很多,因此Gdb的help将其分成了很多种类(class),用户可以通过进一步查看相关class找到相应命令。如下所示:

     

    (gdb) help

    List of classes of commands:

     

    aliases -- Aliases of other commands

    breakpoints -- Making program stop at certain points

    data -- Examining data

    files -- Specifying and examining files

    internals -- Maintenance commands

    Type "help" followed by a class name for a list of commands in that class.

    Type "help" followed by command name for full documentation.

    Command name abbreViations are allowed if unambiguous.

     

    上述列出了Gdb各个分类的命令,注意底部的加粗部分说明其为分类命令。接下来可以具体查找各分类种的命令。如下所示:

     

    (gdb) help data

    Examining data.

     

    List of commands:

     

    call -- Call a function in the program

    delete display -- Cancel some expressions to be displayed when program stops

    delete mem -- Delete memory region

    disable display -- Disable some expressions to be displayed when program stops

    Type "help" followed by command name for full documentation.

    Command name abbreViations are allowed if unambiguous.

     

    至此,若用户想要查找call命令,就可键入“help call”。

     

    (gdb) help call

    Call a function in the program.

    The argument is the function name and arguments, in the notation of the

    current working language. The result is printed and saved in the value

    history, if it is not void.

     

    当然,若用户已知命令名,直接键入“help [command]”也是可以的。

    Gdb中的命令主要分为以下几类:工作环境相关命令、设置断点与恢复命令、源代码查看命令、查看运行数据相关命令及修改运行参数命令。以下就分别对这几类的命令进行讲解。

    1.工作环境相关命令

    Gdb中不仅可以调试所运行的程序,而且还可以对程序相关的工作环境进行相应的设定,甚至还可以使用shell中的命令进行相关的操作,其功能极其强大。表3.10所示列出了Gdb常见工作环境相关命令。

    表3.10 Gdb工作环境相关命令

    命 令 格 式

    含义

    set args运行时的参数

    指定运行时参数,如:set args 2

    show args

    查看设置好的运行参数

    path dir

    设定程序的运行路径

    show paths

    查看程序的运行路径

    set enVironment var [=value]

    设置环境变量

    show enVironment [var]

    查看环境变量

    cd dir

    进入到dir目录,相当于shell中的cd命令

    pwd

    显示当前工作目录

    shell command

    运行shell的command命令

    2.设置断点与恢复命令

    Gdb中设置断点与恢复的常见命令如表3.11所示。

    表3.11 Gdb设置断点与恢复相关命令

    命 令 格 式

    含义

    bnfo b

    查看所设断点

    break 行号或函数名 <条件表达式>

    设置断点

    tbreak 行号或函数名 <条件表达式>

    设置临时断点,到达后被自动删除

    delete [断点号]

    删除指定断点,其断点号为”info b”中的第一栏。若缺省断点号则删除所有断点

    disable [断点号]]

    停止指定断点,使用”info b”仍能查看此断点。同delete一样,省断点号则停止所有断点

    enable [断点号]

    激活指定断点,即激活被disable停止的断点

    condition [断点号] <条件表达式>

    修改对应断点的条件

    ignore [断点号]<num>

    在程序执行中,忽略对应断点num次

    step

    单步恢复程序运行,且进入函数调用

    next

    单步恢复程序运行,但不进入函数调用

    finish

    运行程序,直到当前函数完成返回

    c

    继续执行函数,直到函数结束或遇到新的断点

     

    由于设置断点在Gdb的调试中非常重要,所以在此再着重讲解一下Gdb中设置断点的方法。

    Gdb中设置断点有多种方式:其一是按行设置断点,设置方法在3.5.1节已经指出,在此就不重复了。另外还可以设置函数断点和条件断点,在此结合上一小节的代码,具体介绍后两种设置断点的方法。

    ① 函数断点

    Gdb中按函数设置断点只需把函数名列在命令”b”之后,如下所示:

     

    (gdb) b sum

    Breakpoint 1 at 0x80484ba: file test.c, line 16.

    (gdb) info b

    Num Type Disp Enb Address What

    1 breakpoint keep y 0x080484ba in sum at test.c:16

     

    要注意的是,此时的断点实际是在函数的定义处,也就是在16行处(注意第16行还未执行)。

    ② 条件断点

    Gdb中设置条件断点的格式为:b 行数或函数名 if 表达式。具体实例如下所示:

     

    (gdb) b 8 if i==10

    Breakpoint 1 at 0x804848c: file test.c, line 8.

    (gdb) info b

    Num Type Disp Enb Address What

    1 breakpoint keep y 0x0804848c in main at test.c:8

    stop only if i == 10

    (gdb) r

    Starting program: /home/yul/test

    The sum of 1-m is 1275

     

    Breakpoint 1, main () at test.c:9

    9 n += i;

    (gdb) p i

    $1 = 10

     

    可以看到,该例中在第8行(也就是运行完第7行的for循环)设置了一个“i==0”的条件断点,在程序运行之后可以看出,程序确实在i为10时暂停运行。

    3.Gdb中源码查看相关命令

    在Gdb中可以查看源码以方便其他操作,它的常见相关命令如表3.12所示:

    表3.12 Gdb源码查看相关相关命令

    命 令 格 式

    含义

    list <行号>|<函数名>

    查看指定位置代码

    file [文件名]

    加载指定文件

    forward-search 正则表达式

    源代码前向搜索

    reverse-search 正则表达式

    源代码后向搜索

    dir dir

    停止路径名

    show directories

    显示定义了的源文件搜索路径

    info line

    显示加载到Gdb内存中的代码

    4.Gdb中查看运行数据相关命令

    Gdb中查看运行数据是指当程序处于“运行”或“暂停”状态时,可以查看的变量及表达式的信息,其常见命令如表3.13所示:

    表3.13 Gdb查看运行数据相关命令

    命 令 格 式

    含义

    print 表达式|变量

    查看程序运行时对应表达式和变量的值

    x <n/f/u>

    查看内存变量内容。其中n为整数表示显示内存的长度,f表示显示的格式,u表示从当前地址往后请求显示的字节数

    display 表达式

    设定在单步运行或其他情况中,自动显示的对应表达式的内容

    5.Gdb中修改运行参数相关命令

    Gdb还可以修改运行时的参数,并使该变量按照用户当前输入的值继续运行。它的设置方法为:在单步执行的过程中,键入命令“set 变量=设定值”。这样,在此之后,程序就会按照该设定的值运行了。下面,笔者结合上一节的代码将n的初始值设为4,其代码如下所示:

     

    (Gdb) b 7

    Breakpoint 5 at 0x804847a: file test.c, line 7.

    (Gdb) r

    Starting program: /home/yul/test

    The sum of 1-m is 1275

     

    Breakpoint 5, main () at test.c:7

    7 for(i=1; i<=50; i++)

    (Gdb) set n=4

    (Gdb) c

    Continuing.

    The sum of 1-50 is 1279

     

    Program exited with code 031.

     

    可以看到,最后的运行结果确实比之前的值大了4。

     

     

    Gdb的使用切记点:

    · 在Gcc编译选项中一定要加入”-g”。

    · 只有在代码处于“运行”或“暂停”状态时才能查看变量值。

    · 设置断点后程序在指定行之前停止。

    Make工程管理器

    到此为止,读者已经了解了如何在Linux下使用编辑器编写代码,如何使用Gcc把代码编译成可执行文件,还学习了如何使用Gdb来调试程序,那么,所有的工作看似已经完成了,为什么还需要Make这个工程管理器呢?

    所谓工程管理器,顾名思义,是指管理较多的文件的。读者可以试想一下,有一个上百个文件的代码构成的项目,如果其中只有一个或少数几个文件进行了修改,按照之前所学的Gcc编译工具,就不得不把这所有的文件重新编译一遍,因为编译器并不知道哪些文件是最近更新的,而只知道需要包含这些文件才能把源代码编译成可执行文件,于是,程序员就不能不再重新输入数目如此庞大的文件名以完成最后的编译工作。

    但是,请读者仔细回想一下本书在3.1.2节中所阐述的编译过程,编译过程是分为编译、汇编、链接不同阶段的,其中编译阶段仅检查语法错误以及函数与变量的声明是否正确声明了,在链接阶段则主要完成是函数链接和全局变量的链接。因此,那些没有改动的源代码根本不需要重新编译,而只要把它们重新链接进去就可以了。所以,人们就希望有一个工程管理器能够自动识别更新了的文件代码,同时又不需要重复输入冗长的命令行,这样,Make工程管理器也就应运而生了。

    实际上,Make工程管理器也就是个“自动编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入Makefile文件的内容来执行大量的编译工作。用户只需编写一次简单的编译语句就可以了。它大大提高了实际项目的工作效率,而且几乎所有Linux下的项目编程均会涉及到它,希望读者能够认真学习本节内容。

    Makefile基本结构

    Makefile是Make读入的惟一配置文件,因此本节的内容实际就是讲述Makefile的编写规则。在一个Makefile中通常包含如下内容:

    · 需要由make工具创建的目标体(target),通常是目标文件或可执行文件;

    · 要创建的目标体所依赖的文件(dependency_file);

    · 创建每个目标体时需要运行的命令(command)。

    它的格式为:

     

    target: dependency_files

    command

     

    例如,有两个文件分别为hello.c和hello.h,创建的目标体为hello.o,执行的命令为gcc编译指令:gcc –c hello.c,那么,对应的Makefile就可以写为:

     

    #The simplest example

    hello.o: hello.c hello.h

    gcc –c hello.c –o hello.o

     

    接着就可以使用make了。使用make的格式为:make target,这样make就会自动读入Makefile(也可以是首字母小写makefile)并执行对应target的command语句,并会找到相应的依赖文件。如下所示:

     

    [root@localhost makefile]# make hello.o

    gcc –c hello.c –o hello.o

    [root@localhost makefile]# ls

    hello.c hello.h hello.o Makefile

     

    可以看到,Makefile执行了“hello.o”对应的命令语句,并生成了“hello.o”目标体。

     

     

    注意

    在Makefile中的每一个command前必须有“Tab”符,否则在运行make命令时会出错。

    Makefile变量

    上面示例的Makefile在实际中是几乎不存在的,因为它过于简单,仅包含两个文件和一个命令,在这种情况下完全不必要编写Makefile而只需在Shell中直接输入即可,在实际中使用的Makefile往往是包含很多的文件和命令的,这也是Makefile产生的原因。下面就可给出稍微复杂一些的Makefile进行讲解:

     

    sunq:kang.o yul.o

    Gcc kang.o bar.o -o myprog

    kang.o : kang.c kang.h head.h

    Gcc –Wall –O -g –c kang.c -o kang.o

    yul.o : bar.c head.h

    Gcc - Wall –O -g –c yul.c -o yul.o

     

    在这个Makefile中有三个目标体(target),分别为sunq、kang.o和yul.o,其中第一个目标体的依赖文件就是后两个目标体。如果用户使用命令“make sunq”,则make管理器就是找到sunq目标体开始执行。

    这时,make会自动检查相关文件的时间戳。首先,在检查“kang.o”、“yul.o”和“sunq”三个文件的时间戳之前,它会向下查找那些把“kang.o”或“yul.o”做为目标文件的时间戳。比如,“kang.o”的依赖文件为:“kang.c”、“kang.h”、“head.h”。如果这些文件中任何一个的时间戳比“kang.o”新,则命令“gcc –Wall –O -g –c kang.c -o kang.o”将会执行,从而更新文件“kang.o”。在更新完“kang.o”或“yul.o”之后,make会检查最初的“kang.o”、“yul.o”和“sunq”三个文件,只要文件“kang.o”或“yul.o”中的任比文件时间戳比“sunq”新,则第二行命令就会被执行。这样,make就完成了自动检查时间戳的工作,开始执行编译工作。这也就是Make工作的基本流程。

    接下来,为了进一步简化编辑和维护Makefile,make允许在Makefile中创建和使用变量。变量是在Makefile中定义的名字,用来代替一个文本字符串,该文本字符串称为该变量的值。在具体要求下,这些值可以代替目标体、依赖文件、命令以及makefile文件中其它部分。在Makefile中的变量定义有两种方式:一种是递归展开方式,另一种是简单方式。

    递归展开方式定义的变量是在引用在该变量时进行替换的,即如果该变量包含了对其他变量的应用,则在引用该变量时一次性将内嵌的变量全部展开,虽然这种类型的变量能够很好地完成用户的指令,但是它也有严重的缺点,如不能在变量后追加内容(因为语句:CFLAGS = $(CFLAGS) -O在变量扩展过程中可能导致无穷循环)。

    为了避免上述问题,简单扩展型变量的值在定义处展开,并且只展开一次,因此它不包含任何对其它变量的引用,从而消除变量的嵌套引用。

    递归展开方式的定义格式为:VAR=var

    简单扩展方式的定义格式为:VAR:=var

    Make中的变量使用均使用格式为:$(VAR)

     

     

    注意

    变量名是不包括“:”、“#”、“=”结尾空格的任何字符串。同时,变量名中包含字母、数字以及下划线以外的情况应尽量避免,因为它们可能在将来被赋予特别的含义。

    变量名是大小写敏感的,例如变量名“foo”、“FOO”、和“Foo”代表不同的变量。

    推荐在makefile内部使用小写字母作为变量名,预留大写字母作为控制隐含规则参数或用户重载命令选项参数的变量名。

     

    下面给出了上例中用变量替换修改后的Makefile,这里用OBJS代替kang.o和yul.o,用CC代替Gcc,用CFLAGS代替“-Wall -O –g”。这样在以后修改时,就可以只修改变量定义,而不需要修改下面的定义实体,从而大大简化了Makefile维护的工作量。

    经变量替换后的Makefile如下所示:

     

    OBJS = kang.o yul.o

    CC = Gcc

    CFLAGS = -Wall -O -g

    sunq : $(OBJS)

    $(CC) $(OBJS) -o sunq

    kang.o : kang.c kang.h

    $(CC) $(CFLAGS) -c kang.c -o kang.o

    yul.o : yul.c yul.h

    $(CC) $(CFLAGS) -c yul.c -o yul.o

     

    可以看到,此处变量是以递归展开方式定义的。

    Makefile中的变量分为用户自定义变量、预定义变量、自动变量及环境变量。如上例中的OBJS就是用户自定义变量,自定义变量的值由用户自行设定,而预定义变量和自动变量为通常在Makefile都会出现的变量,其中部分有默认值,也就是常见的设定值,当然用户可以对其进行修改。

    预定义变量包含了常见编译器、汇编器的名称及其编译选项。下表3.14列出了Makefile中常见预定义变量及其部分默认值。

    表3.14 Makefile中常见预定义变量

    命 令 格 式

    含义

    AR

    库文件维护程序的名称,默认值为ar

    AS

    汇编程序的名称,默认值为as

    CC

    C编译器的名称,默认值为cc

    CPP

    C预编译器的名称,默认值为$(CC) –E

    CXX

    C++编译器的名称,默认值为g++

    FC

    FORTRAN编译器的名称,默认值为f77

    RM

    文件删除程序的名称,默认值为rm –f

    ARFLAGS

    库文件维护程序的选项,无默认值

    ASFLAGS

    汇编程序的选项,无默认值

    CFLAGS

    C编译器的选项,无默认值

    CPPFLAGS

    C预编译的选项,无默认值

    CXXFLAGS

    C++编译器的选项,无默认值

    FFLAGS

    FORTRAN编译器的选项,无默认值

     

    可以看出,上例中的CC和CFLAGS是预定义变量,其中由于CC没有采用默认值,因此,需要把“CC=Gcc”明确列出来。

    由于常见的Gcc编译语句中通常包含了目标文件和依赖文件,而这些文件在Makefile文件中目标体的一行已经有所体现,因此,为了进一步简化Makefile的编写,就引入了自动变量。自动变量通常可以代表编译语句中出现目标文件和依赖文件等,并且具有本地含义(即下一语句中出现的相同变量代表的是下一语句的目标文件和依赖文件)。下表3.15列出了Makefile中常见自动变量。

    表3.15Makefile中常见自动变量

    命令格式

    含义

    $*

    不包含扩展名的目标文件名称

    $+

    所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件

    $<

    第一个依赖文件的名称

    $?

    所有时间戳比目标文件晚的依赖文件,并以空格分开

    命令格式

    含义

    $@

    目标文件的完整名称

    $^

    所有不重复的依赖文件,以空格分开

    $%

    如果目标是归档成员,则该变量表示目标的归档成员名称

     

    自动变量的书写比较难记,但是在熟练了之后会非常的方便,请读者结合下例中的自动变量改写的Makefile进行记忆。

     

    OBJS = kang.o yul.o

    CC = Gcc

    CFLAGS = -Wall -O -g

    sunq : $(OBJS)

    $(CC) $^ -o $@

    kang.o : kang.c kang.h

    $(CC) $(CFLAGS) -c $< -o $@

    yul.o : yul.c yul.h

    $(CC) $(CFLAGS) -c $< -o $@

     

    另外,在Makefile中还可以使用环境变量。使用环境变量的方法相对比较简单,make在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量。但是,如果用户在Makefile中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量。

    Makefile规则

    Makefile的规则是Make进行处理的依据,它包括了目标体、依赖文件及其之间的命令语句。一般的,Makefile中的一条语句就是一个规则。在上面的例子中,都显示地指出了Makefile中的规则关系,如“$(CC) $(CFLAGS) -c $< -o $@”,但为了简化Makefile的编写,make还定义了隐式规则和模式规则,下面就分别对其进行讲解。

    1.隐式规则

    隐含规则能够告诉make怎样使用传统的技术完成任务,这样,当用户使用它们时就不必详细指定编译的具体细节,而只需把目标文件列出即可。Make会自动搜索隐式规则目录来确定如何生成目标文件。如上例就可以写成:

     

    OBJS = kang.o yul.o

    CC = Gcc

    CFLAGS = -Wall -O -g

    sunq : $(OBJS)

    $(CC) $^ -o $@

     

    为什么可以省略后两句呢?因为Make的隐式规则指出:所有“.o”文件都可自动由“.c”文件使用命令“$(CC) $(CPPFLAGS) $(CFLAGS) -c file.c –o file.o”生成。这样“kang.o”和“yul.o”就会分别调用“$(CC) $(CFLAGS) -c kang.c -o kang.o”和“$(CC) $(CFLAGS) -c yul.c -o yul.o”生成。

     

     

    注意

    在隐式规则只能查找到相同文件名的不同后缀名文件,如”kang.o”文件必须由”kang.c”文件生成。

     

    下表3.16给出了常见的隐式规则目录:

    表3.16 Makefile中常见隐式规则目录

    对应语言后缀名

    规则

    C编译:.c变为.o

    $(CC) –c $(CPPFLAGS) $(CFLAGS)

    C++编译:.cc或.C变为.o

    $(CXX) -c $(CPPFLAGS) $(CXXFLAGS)

    Pascal编译:.p变为.o

    $(PC) -c $(PFLAGS)

    Fortran编译:.r变为-o

    $(FC) -c $(FFLAGS)

    2.模式规则

    模式规则是用来定义相同处理规则的多个文件的。它不同于隐式规则,隐式规则仅仅能够用make默认的变量来进行操作,而模式规则还能引入用户自定义变量,为多个文件建立相同的规则,从而简化Makefile的编写。

    模式规则的格式类似于普通规则,这个规则中的相关文件前必须用“%”标明。使用模式规则修改后的Makefile的编写如下:

     

    OBJS = kang.o yul.o

    CC = Gcc

    CFLAGS = -Wall -O -g

    sunq : $(OBJS)

    $(CC) $^ -o $@

    %.o : %.c

    $(CC) $(CFLAGS) -c $< -o $@

    Make使用

    使用make管理器非常简单,只需在make命令的后面键入目标名即可建立指定的目标,如果直接运行make,则建立Makefile中的第一个目标。

    此外make还有丰富的命令行选项,可以完成各种不同的功能。下表3.17列出了常用的make命令行选项。

    表3.17 make的命令行选项

    命令格式

    含 义

    -C dir

    读入指定目录下的Makefile

    -f file

    读入当前目录下的file文件作为Makefile

    命令格式

    含 义

    -i

    忽略所有的命令执行错误

    -I dir

    指定被包含的Makefile所在目录

    -n

    只打印要执行的命令,但不执行这些命令

    -p

    显示make变量数据库和隐含规则

    -s

    在执行命令时不显示命令

    -w

    如果make在执行过程中改变目录,则打印当前目录名

    使用autotools

    在上一小节,读者已经了解到了make项目管理器的强大功能。的确,Makefile可以帮助make完成它的使命,但要承认的是,编写Makefile确实不是一件轻松的事,尤其对于一个较大的项目而言更是如此。那么,有没有一种轻松的手段生成Makefile而同时又能让用户享受make的优越性呢?本节要讲的autotools系列工具正是为此而设的,它只需用户输入简单的目标文件、依赖文件、文件目录等就可以轻松地生成Makefile了,这无疑是广大用户的所希望的。另外,这些工具还可以完成系统配置信息的收集,从而可以方便地处理各种移植性的问题。也正是基于此,现在Linux上的软件开发一般都用autotools来制作Makefile,读者在后面的讲述中就会了解到。

    autotools使用流程

    正如前面所言,autotools是系列工具,读者首先要确认系统是否装了以下工具(可以用which命令进行查看)。

    · aclocal

    · autoscan

    · autoconf

    · autoheader

    · automake

    使用autotools主要就是利用各个工具的脚本文件以生成最后的Makefile。其总体流程是这样的:

    · 使用aclocal生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义;

    · 改写“configure.scan”文件,并将其重命名为“configure.in”,并使用autoconf文件生成configure文件。

    接下来,笔者将通过一个简单的hello.c例子带领读者熟悉autotools生成makefile的过程,由于在这过程中有涉及到较多的脚本文件,为了更清楚地了解相互之间的关系,强烈建议读者实际动手操作以体会其整个过程。

    1.autoscan

    它会在给定目录及其子目录树中检查源文件,若没有给出目录,就在当前目录及其子目录树中进行检查。它会搜索源文件以寻找一般的移植性问题并创建一个文件“configure.scan”,该文件就是接下来autoconf要用到的“configure.in”原型。如下所示:

     

    [root@localhost automake]# autoscan

    autom4te: configure.ac: no such file or directory

    autoscan: /usr/bin/autom4te failed with exit status: 1

    [root@localhost automake]# ls

    autoscan.log configure.scan hello.c

     

    如上所示,autoscan首先会尝试去读入“configure.ac”(同configure.in的配置文件)文件,此时还没有创建该配置文件,于是它会自动生成一个“configure.in”的原型文件“configure.scan”。

    2.autoconf

    configure.in是autoconf的脚本配置文件,它的原型文件“configure.scan”如下所示:

     

    # -*- Autoconf -*-

    # Process this file with autoconf to produce a configure script.

    AC_PREREQ(2.59)

    #The next one is modified by sunq

    #AC_INIT(FULL-PACKAGE-NAME,VERSION,BUG-REPORT-ADDRESS)

    AC_INIT(hello,1.0)

    # The next one is added by sunq

    AM_INIT_AUTOMAKE(hello,1.0)

    AC_CONFIG_SRCDIR([hello.c])

    AC_CONFIG_HEADER([config.h])

    # Checks for programs.

    AC_PROG_CC

    # Checks for libraries.

    # Checks for header files.

    # Checks for typedefs, structures, and compiler characteristics.

    # Checks for library functions.

    AC_CONFIG_FILES([Makefile])

    AC_OUTPUT

     

    下面对这个脚本文件进行解释:

    · 以“#”号开始的行为注释。

    · AC_PREREQ宏声明本文件要求的autoconf版本,如本例使用的版本2.59。

    · AC_INIT宏用来定义软件的名称和版本等信息,在本例中省略了BUG-REPORT-ADDRESS,一般为作者的e-mail。

    · AM_INIT_AUTOMAKE是笔者另加的,它是automake所必备的宏,也同前面一样,PACKAGE是所要产生软件套件的名称,VERSION是版本编号。

    · AC_CONFIG_SRCDIR宏用来侦测所指定的源码文件是否存在,来确定源码目录的有

    效性。在此处为当前目录下的hello.c。

    · AC_CONFIG_HEADER宏用于生成config.h文件,以便autoheader使用。

    · AC_CONFIG_FILES宏用于生成相应的Makefile文件。

    · 中间的注释间可以添加分别用户测试程序、测试函数库、测试头文件等宏定义。

    接下来首先运行aclocal,生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义。如下所示:

     

    [root@localhost automake]# aclocal

     

    再接着运行autoconf,生成“configure”可执行文件。如下所示:

     

    [root@localhost automake]# autoconf

    [root@localhost automake]# ls

    aclocal.m4 autom4te.cache autoscan.log configure configure.in hello.c

    3.autoheader

    接着使用autoheader命令,它负责生成config.h.in文件。该工具通常会从“acconfig.h”文件中复制用户附加的符号定义,因此此处没有附加符号定义,所以不需要创建“acconfig.h”文件。如下所示:

     

    [root@localhost automake]# autoheader

    4.automake

    这一步是创建Makefile很重要的一步,automake要用的脚本配置文件是Makefile.am,用户需要自己创建相应的文件。之后,automake工具转换成Makefile.in。在该例中,笔者创建的文件为Makefile.am如下所示:

     

    AUTOMAKE_OPTIONS=foreign

    bin_PROGRAMS= hello

    hello_SOURCES= hello.c

     

    下面对该脚本文件的对应项进行解释。

    · 其中的AUTOMAKE_OPTIONS为设置automake的选项。由于GNU(在第1章中已经有所介绍)对自己发布的软件有严格的规范,比如必须附带许可证声明文件COPYING等,否则automake执行时会报错。automake提供了三种软件等级:foreign、gnu和gnits,让用户选择采用,默认等级为gnu。在本例使用foreign等级,它只检测必须的文件。

    · bin_PROGRAMS定义要产生的执行文件名。如果要产生多个执行文件,每个文件名用空格隔开。

    · hello_SOURCES定义“hello”这个执行程序所需要的原始文件。如果”hello”这个程序是由多个原始文件所产生的,则必须把它所用到的所有原始文件都列出来,并用空格隔开。例如:若目标体“hello”需要“hello.c”、“sunq.c”、“hello.h”三个依赖文件,则定义hello_SOURCES=hello.c sunq.c hello.h。要注意的是,如果要定义多个执行文件,则对每个执行程序都要定义相应的file_SOURCES。

    接下来可以使用automake对其生成“configure.in”文件,在这里使用选项“—adding-missing”可以让automake自动添加有一些必需的脚本文件。如下所示:

     

    [root@localhost automake]# automake --add-missing

    configure.in: installing './install-sh'

    configure.in: installing './missing'

    Makefile.am: installing 'depcomp'

    [root@localhost automake]# ls

    aclocal.m4 autoscan.log configure.in hello.c Makefile.am missing

    autom4te.cache configure depcomp install-sh Makefile.in config.h.in

     

    可以看到,在automake之后就可以生成configure.in文件。

    5.运行configure

    在这一步中,通过运行自动配置设置文件configure,把Makefile.in变成了最终的Makefile。如下所示:

     

    [root@localhost automake]# ./configure

    checking for a BSD-compatible install... /usr/bin/install -c

    checking whether build enVironment is sane... yes

    checking for gawk... gawk

    checking whether make sets $(MAKE)... yes

    checking for Gcc... Gcc

    checking for C compiler default output file name... a.out

    checking whether the C compiler works... yes

    checking whether we are cross compiling... no

    checking for suffix of executables...

    checking for suffix of object files... o

    checking whether we are using the GNU C compiler... yes

    checking whether Gcc accepts -g... yes

    checking for Gcc option to accept ANSI C... none needed

    checking for style of include used by make... GNU

    checking dependency style of Gcc... Gcc3

    configure: creating ./config.status

    config.status: creating Makefile

    config.status: executing depfiles commands

    可以看到,在运行configure时收集了系统的信息,用户可以在configure命令中对其进行方便地配置。在./configure的自定义参数有两种,一种是开关式(--enable-XXX或--disable-XXX),另一种是开放式,即后面要填入一串字符(--with-XXX=yyyy)参数。读者可以自行尝试其使用方法。另外,读者可以查看同一目录下的”config.log”文件,以方便调试之用。

    到此为止,makefile就可以自动生成了。回忆整个步骤,用户不再需要定制不同的规则,而只需要输入简单的文件及目录名即可,这样就大大方便了用户的使用。下面的图3.9总结了上述过程:

     

    图3.9 autotools生成Makefile流程图

    使用autotools所生成的Makefile

    autotools生成的Makefile除具有普通的编译功能外,还具有以下主要功能(感兴趣的读者可以查看这个简单的hello.c程序的makefile):

    1.make

    键入make默认执行”make all”命令,即目标体为all,其执行情况如下所示:

     

    [root@localhost automake]# make

    if Gcc -DPACKAGE_NAME="" -DPACKAGE_TARNAME="" -DPACKAGE_VERSION="" -DPACKAGE_STRING="" -DPACKAGE_BUGREPORT="" -DPACKAGE="hello" -DVERSION="1.0" -I. -I. -g -O2 -MT hello.o -MD -MP -MF ".deps/hello.Tpo" -c -o hello.o hello.c;

    then mv -f ".deps/hello.Tpo" ".deps/hello.Po"; else rm -f ".deps/hello.Tpo"; exit 1; fi

    Gcc -g -O2 -o hello hello.o

    此时在本目录下就生成了可执行文件“hello”,运行“./hello”能出现正常结果,如下所示:

     

    [root@localhost automake]# ./hello

    Hello!Autoconf!

    2.make install

    此时,会把该程序安装到系统目录中去,如下所示:

     

    [root@localhost automake]# make install

    if Gcc -DPACKAGE_NAME="" -DPACKAGE_TARNAME="" -DPACKAGE_VERSION="" -DPACKAGE_STRING="" -DPACKAGE_BUGREPORT="" -DPACKAGE="hello" -DVERSION="1.0" -I. -I. -g -O2 -MT hello.o -MD -MP -MF ".deps/hello.Tpo" -c -o hello.o hello.c;

    then mv -f ".deps/hello.Tpo" ".deps/hello.Po"; else rm -f ".deps/hello.Tpo"; exit 1; fi

    Gcc -g -O2 -o hello hello.o

    make[1]: Entering directory '/root/workplace/automake'

    test -z "/usr/local/bin" || mkdir -p -- "/usr/local/bin"

    /usr/bin/install -c 'hello' '/usr/local/bin/hello'

    make[1]: Nothing to be done for 'install-data-am'.

    make[1]: LeaVing directory '/root/workplace/automake'

     

    此时,若直接运行hello,也能出现正确结果,如下所示:

     

    [root@localhost automake]# hello

    Hello!Autoconf!

    3.make clean

    此时,make会清除之前所编译的可执行文件及目标文件(object file, *.o),如下所示:

     

    [root@localhost automake]# make clean

    test -z "hello" || rm -f hello

    rm -f *.o

    4.make dist

    此时,make将程序和相关的文档打包为一个压缩文档以供发布,如下所示:

     

    [root@localhost automake]# make dist

    [root@localhost automake]# ls hello-1.0-tar.gz

    hello-1.0-tar.gz

     

    可见该命令生成了一个hello-1.0-tar.gz的压缩文件。

    由上面的讲述读者不难看出,autotools确实是软件维护与发布的必备工具,也鉴于此,如今GUN的软件一般都是由automake来制作的。

     

     

    想一想

    对于automake制作的这类软件,应如何安装呢?

    Vi使用练习

    1.实验目的

    通过指定指令的Vi操作练习,使读者能够熟练使用Vi中的常见操作,并且熟悉Vi的三种模式,如果读者能够熟练掌握实验内容中所要求的内容,则表明对Vi的操作已经很熟练了。

    2.实验内容

    (1)在“/root”目录下建一个名为“/Vi”的目录。

    (2)进入“/Vi”目录。

    (3)将文件“/etc/inittab”复制到“/Vi”目录下。

    (4)使用Vi打开“/Vi”目录下的inittab。

    (5)设定行号,指出设定initdefault(类似于“id:5:initdefault”)的所在行号。

    (6)将光标移到该行。

    (7)复制该行内容。

    (8)将光标移到最后一行行首。

    (9)粘贴复制行的内容。

    (10)撤销第9步的动作。

    (11)将光标移动到最后一行的行尾。

    (12)粘贴复制行的内容。

    (13)光标移到“si::sysinit:/etc/rc.d/rc.sysinit”。

    (14)删除该行。

    (15)存盘但不退出。

    (16)将光标移到首行。

    (17)插入模式下输入“Hello,this is Vi world!”。

    (18)返回命令行模式。

    (19)向下查找字符串“0:wait”。

    (20)再向上查找字符串“halt”。

    (21)强制退出Vi,不存盘。

    分别指出每个命令处于何种模式下?

    3.实验步骤

    (1)mkdir /root/Vi

    (2)cd /root/Vi

    (3)cp /etc/inittab ./

    (4)Vi ./inittab

    (5):set nu(底行模式)

    (6)17<enter>(命令行模式)

    (7)yy

    (8)G

    (9)p

    (10)u

    (11)$

    (12)p

    (13)21G

    (14)dd

    (15):w(底行模式)

    (16)1G

    (17)i 并输入“Hello,this is Vi world!”(插入模式)

    (18)Esc

    (19)/0:wait(命令行模式)

    (20)?halt

    (21):q!(底行模式)

    4.实验结果

    该实验最后的结果只对“/root/inittab”增加了一行复制的内容:“id:5:initdefault”。

    用Gdb调试有问题的程序

    1.实验目的

    通过调试一个有问题的程序,使读者进一步熟练使用Vi操作,而且熟练掌握Gcc编译命令及Gdb的调试命令,通过对有问题程序的跟踪调试,进一步提高发现问题和解决问题的能力。这是一个很小的程序,只有35行,希望读者认真调试。

    2.实验内容

    (1)使用Vi编辑器,将以下代码输入到名为greet.c的文件中。此代码的原意为输出倒序main函数中定义的字符串,但结果显示没有输出。代码如下所示:

     

    #include <stdio.h>

    int display1(char *string);

    int display2(char *string);

     

    int main ()

    {

    char string[] = "Embedded Linux";

    display1 (string);

    display2 (string);

    }

    int display1 (char *string)

    {

    printf ("The original string is %s n", string);

    }

    int display2 (char *string1)

    {

    char *string2;

    int size,i;

    size = strlen (string1);

    string2 = (char *) malloc (size + 1);

    for (i = 0; i < size; i++)

    string2[size - i] = string1[i];

    string2[size+1] = ' ';

    printf("The string afterward is %sn",string2);

    }

     

    (2)使用Gcc编译这段代码,注意要加上“-g”选项以方便之后的调试。

    (3)运行生成的可执行文件,观察运行结果。

    (4)使用Gdb调试程序,通过设置断点、单步跟踪,一步步找出错误所在。

    (5)纠正错误,更改源程序并得到正确的结果。

    3.实验步骤

    (1)在工作目录上新建文件greet.c,并用Vi启动:vi greet.c。

    (2)在Vi中输入以上代码。

    (3)在Vi中保存并退出:wq。

    (4)用Gcc编译:gcc -g greet.c -o greet。

    (5)运行greet:./greet,输出为:

     

    The original string is Embedded Linux

    The string afterward is

     

    可见,该程序没有能够倒序输出。

    (6)启动Gdb调试:gdb greet。

    (7)查看源代码,使用命令“l”。

    (8)在30行(for循环处)设置断点,使用命令“b 30”。

    (9)在33行(printf函数处)设置断点,使用命令“b 33”。

    (10)查看断点设置情况,使用命令“info b”。

    (11)运行代码,使用命令“r”。

    (12)单步运行代码,使用命令“n”。

    (13)查看暂停点变量值,使用命令“p string2[size - i]”。

    (14)继续单步运行代码数次,并使用命令查看,发现string2[size-1]的值正确。

    (15)继续程序的运行,使用命令“c”。

    (16)程序在printf前停止运行,此时依次查看string2[0]、string2[1]…,发现string[0]没有被正确赋值,而后面的复制都是正确的,这时,定位程序第31行,发现程序运行结果错误的原因在于“size-1”。由于i只能增到“size-1”,这样string2[0]就永远不能被赋值而保持NULL,故输不出任何结果。

    (17)退出Gdb,使用命令q。

    (18)重新编辑greet.c,把其中的“string2[size - i] = string1[i]”改为“string2[size – i - 1] = string1[i];”即可。

    (19)使用Gcc重新编译:gcc -g greet.c -o greet。

    (20)查看运行结果:./greet

     

    The original string is Embedded Linux

    The string afterward is xuniL deddedbmE

     

    这时,输入结果正确。

    4.实验结果

    将原来有错的程序经过Gdb调试,找出问题所在,并修改源代码,输出正确的倒序显示字符串的结果。

    编写包含多文件的Makefile

    1.实验目的

    通过对包含多文件的Makefile的编写,熟悉各种形式的Makefile,并且进一步加深对Makefile中用户自定义变量、自动变量及预定义变量的理解。

    2.实验过程

    (1)用Vi在同一目录下编辑两个简单的Hello程序,如下所示:

     

    #hello.c

    #include "hello.h"

    int main()

    {

    printf("Hello everyone!n");

    }

    #hello.h

    #include <stdio.h>

     

    (2)仍在同一目录下用Vi编辑Makefile,且不使用变量替换,用一个目标体实现(即直接将hello.c和hello.h编译成hello目标体)。然后用make验证所编写的Makefile是否正确。

    (3)将上述Makefile使用变量替换实现。同样用make验证所编写的Makefile是否正确

    (4)用编辑另一Makefile,取名为Makefile1,不使用变量替换,但用两个目标体实现(也就是首先将hello.c和hello.h编译为hello.o,再将hello.o编译为hello),再用make的”-f”选项验证这个Makefile1的正确性。

    (5)将上述Makefile1使用变量替换实现。

    3.实验步骤

    (1)用Vi打开上述两个代码文件“hello.c”和“hello.h”。

    (2)在shell命令行中用Gcc尝试编译,使用命令:”Gcc hello.c –o hello”,并运行hello可执行文件查看结果。

    (3)删除此次编译的可执行文件:rm hello。

    (4)用Vi编辑Makefile,如下所示:

     

    hello:hello.c hello.h

    Gcc hello.c -o hello

     

    (5)退出保存,在shell中键入:make,查看结果。

    (6)再次用Vi打开Makefile,用变量进行替换,如下所示:

     

    OBJS :=hello.o

    CC :=Gcc

    hello:$(OBJS)

    $(CC) $^ -o $@

     

    (7)退出保存,在shell中键入:make,查看结果。

    (8)用Vi编辑Makefile1,如下所示:

     

    hello:hello.o

    Gcc hello.o -o hello

    hello.o:hello.c hello.h

    Gcc -c hello.c -o hello.o

     

    (9)退出保存,在shell中键入:make -f Makefile1,查看结果。

    (10)再次用Vi编辑Makefile1,如下所示:

     

    OBJS1 :=hello.o

    OBJS2 :=hello.c hello.h

    CC :=Gcc

    hello:$(OBJS1)

    $(CC) $^ -o $@

    $(OBJS1):$(OBJS2)

    $(CC) -c $< -o $@

     

    在这里请注意区别“$^”和“$<”。

    (11)退出保存,在shell中键入:make -f Makefile1,查看结果

    4.实验结果

    各种不同形式的makefile都能完成其正确的功能。

    使用autotools生成包含多文件的Makefile

    1.实验目的

    通过使用autotools生成包含多文件的Makefile,进一步掌握autotools的正确使用方法。同时,掌握Linux下安装软件的常用方法。

    2.实验过程

    (1)在原目录下新建文件夹auto。

    (2)利用上例的两个代码文件“hello.c”和“hello.h”,并将它们复制到该目录下。

    (3)使用autoscan生成configure.scan。

    (4)编辑configure.scan,修改相关内容,并将其重命名为configure.in。

    (5)使用aclocal生成aclocal.m4。

    (6)使用autoconf生成configure。

    (7)使用autoheader生成config.in.h。

    (8)编辑Makefile.am。

    (9)使用automake生成Makefile.in。

    (10)使用configure生成Makefile。

    (11)使用make生成hello可执行文件,并在当前目录下运行hello查看结果。

    (12)使用make install将hello安装到系统目录下,并运行,查看结果。

    (13)使用make dist生成hello压缩包。

    (14)解压hello压缩包。

    (15)进入解压目录。

    (16)在该目录下安装hello软件。

    3.实验步骤

    (1)mkdir ./auto。

    (2)cp hello.* ./auto(假定原先在“hello.c”文件目录下)。

    (3)命令:autoscan。

    (4)使用Vi编辑configure.scan为:

     

    # -*- Autoconf -*-

    # Process this file with autoconf to produce a configure script.

     

    AC_PREREQ(2.59)

    AC_INIT(hello, 1.0)

    AM_INIT_AUTOMAKE(hello,1.0)

    AC_CONFIG_SRCDIR([hello.h])

    AC_CONFIG_HEADER([config.h])

    # Checks for programs.

    AC_PROG_CC

    # Checks for libraries.

    # Checks for header files.

    # Checks for typedefs, structures, and compiler characteristics.

    # Checks for library functions.

    AC_OUTPUT(Makefile)

     

    (5)保存退出,并重命名为configure.in。

    (6)运行:aclocal。

    (7)运行:autoconf,并用ls查看是否生成了configure可执行文件。

    (8)运行:autoheader。

    (9)用Vi编辑Makefile.am文件为:

     

    AUTOMAKE_OPTIONS=foreign

    bin_PROGRAMS=hello

    hello_SOURCES=hello.c hello.h

     

    (10)运行:automake。

    (11)运行:./configure。

    (12)运行:make。

    (13)运行:./hello,查看结果是否正确。

    (14)运行:make install。

    (15)运行:hello,查看结果是否正确。

    (16)运行:make dist。

    (17)在当前目录下解压hello-1.0.tar.gz:tar –zxvf hello-1.0.tar.gz。

    (18)进入解压目录:cd ./hello-1.0。

    (19)下面开始Linux下常见的安装软件步骤:./configure。

    (20)运行:make。

    (21)运行:./hello(在正常安装时这一步可省略)。

    (22)运行:make install。

    (23)运行:hello,查看结果是否正确。

    4.实验结果

    能够正确使用autotools生成Makefile,并且能够安装成功短小的Hello软件。


    展开全文
  • Linux下使用ifconfigl命令能很方便的查看网卡与网线是否连通,运行ifconfig eth0命令大致输出如下: # ifconfig eth0 eth0 Link encap:Ethernet HWaddr 00:25:35:68:CC:D6   inet addr:192.168.1.168 ...

    在Linux下使用ifconfigl命令能很方便的查看网卡与网线是否连通,运行ifconfig eth0命令大致输出如下:
    # ifconfig eth0
    eth0      Link encap:Ethernet  HWaddr 00:25:35:68:CC:D6  
              inet addr:192.168.1.168  Bcast:192.168.1.255  Mask:255.255.255.0 

              inet6 addr: fe80::215:c5ff:fe18:ccd6/64 Scope:Link
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:130722 errors:0 dropped:0 overruns:0 frame:0
              TX packets:112560 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000 
              RX bytes:104371099 (99.5 MiB)  TX bytes:20518584 (19.5 MiB)
              Interrupt:16 

    其中的RUNNING就表示网卡与网线正常链接,拔掉网线再运行此命令就会发现RUNNING不在了。

        我的目的是用C语言来实现程序,而Linux系统提供了popen/pclose进程管道让C和shell很方便的交互,不过使用的时候要注意设置权限,以免造成安全隐患。废话不多说,看下面C代码结合shell命令检测网卡与网线连通状况:
    netstat.c 
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>

    /**********************************************************************
    * 函数名称: GetNetStat
    * 功能描述: 检测网络链接是否断开
    * 输入参数: 
    * 输出参数: 无
    * 返 回 值: 正常链接1,断开返回-1
    * 其它说明: 本程序需要超级用户权限才能成功调用ifconfig命令
    * 修改日期        版本号     修改人          修改内容
    * ---------------------------------------------------------------------
    * 2010/04/02      V1.0      eden_mgqw
    ***********************************************************************/ 
    int GetNetStat( )
    {
        char    buffer[BUFSIZ];
        FILE    *read_fp;
        int        chars_read;
        int        ret;
        
        memset( buffer, 0, BUFSIZ );
        read_fp = popen("ifconfig eth0 | grep RUNNING", "r");
        if ( read_fp != NULL ) 
        {
            chars_read = fread(buffer, sizeof(char), BUFSIZ-1, read_fp);
            if (chars_read > 0) 
            {
                ret = 1;
            }
            else
            {
                ret = -1;
            }
            pclose(read_fp);
        }
        else
        {
            ret = -1;
        }

        return ret;
    }


    int main()
    {
        int i=0;
        i = GetNetStat();
        printf( "\nNetStat = %d\n", i );
        return 0;
    }

     

    下面是编译运行程序的输出结果(正常返回1,断开返回-1):
    # cc netstat.c 
    # ./a.out 
    NetStat = 1

    SIOCETHTOOL命令字。



    link_stat.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    #include <fcntl.h>
    #include <errno.h>
    #include <sys/ioctl.h>

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/if.h>
    #include <linux/sockios.h>
    #include <linux/ethtool.h>

    int get_netlink_status(const char *if_name);

    int main()
    {
        if(getuid() != 0)
        {
            fprintf(stderr, "Netlink Status Check Need Root Power.\n");
            return 1;
        }
        
        printf("Net link status: %d\n", get_netlink_status("eth0"));

        return 0;
    }

    // if_name like "ath0", "eth0". Notice: call this function
    // need root privilege.
    // return value:
    // -1 -- error , details can check errno
    // 1 -- interface link up
    // 0 -- interface link down.
    int get_netlink_status(const char *if_name)
    {
        int skfd;
        struct ifreq ifr;
        struct ethtool_value edata;

        edata.cmd = ETHTOOL_GLINK;
        edata.data = 0;

        memset(&ifr, 0, sizeof(ifr));
        strncpy(ifr.ifr_name, if_name, sizeof(ifr.ifr_name) - 1);
        ifr.ifr_data = (char *) &edata;

        if (( skfd = socket( AF_INET, SOCK_DGRAM, 0 )) < 0)
            return -1;

        if(ioctl( skfd, SIOCETHTOOL, &ifr ) == -1)
        {
            close(skfd);
            return -1;
        }

        close(skfd);
        return edata.data;
    }

    展开全文
  • gcc是GNU Compiler Collection,原名为Gun C语言编译器,因为它原本只能处理C语言,但gcc很快地...而 Linux cc 一般是一个符号连接,指向 gcc;可以通过$ ls -l /usr/bin/cc来简单查看,该变量是 make 程序的内建...
  • Linux下计算程序运行时间的两种方法

    千次阅读 2013-09-23 16:36:59
    转自:http://hi.baidu.com/starflier/item/14057315de8fb8cc38cb30bb 有时候我们需要计算程序...在linux下有下面两种计算程序运行时间的方法: 方法一: #include int gettimeofday(struct timeval
  • ASK:Linux下通过源码编译安装程序(configure/make/make install的作用) configure Linux 平台有各种不同的配置,安装时需要通过 configure来确定,如:编译器用的是 cc 还是 gcc、不同库文件所在目录等。执行...
  • 把windows下的程序挪到linux下,代码见这篇随笔 1、编译ZTHREAD,使用./configure失败,直接编译的,方法还是上面提到的那篇随笔 2、编译源码,最开始用的cc,后来老是报undefined reference,甚至hello world程序...
  • 此文章转自他人,只是...Linux下程序中如何进行繁体中文和简体中文的转换   首先发个牢骚吧,简繁转换的问题让我在网上搜了好久都没有好的答案,得到的回答尽是让我建个简繁体字的对照表然后自己查表。我就不信
  • 下面是我在redhat下编写的一个小程序hello在linux下编写的c++程序一般用 .cpp 结尾编译的时候用g++而不用cc下面是整个程序#includeusing namespace std;using std::cin;using std::cout;int main(void){ cout char
  • 第一步: 安装mingw sudo apt-get install mingw32 mingw32-binutils ...第二部: 编写纯c程序代码通过GCC/CC编译调试,用mingw输出win目标文件 i586-mingw32msvc-gcc1.c -o 1.exe mingw相关命令 C compiler: i586-m...
  • Linux环境调试C程序

    2016-03-18 14:36:45
     要调试C程序,在编译时,必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如:  > cc -g hello.c -o hello  > g++ -g hello.cpp -o hello  如果没有-g,你将看不见程序...
  • linux编译C程序

    2019-10-27 08:59:16
    1.在终端打开一个编辑器(我用的gedit),建立一个hello.c文件并保存, 2.关闭编辑器,在终端上输入 gcc -o hello hello.c编译c程序 3.输入./hello运行c程序 ——————————————— 版权声明:本文为CSDN博主...
  • GNU早就组织开发了一套C语言编译器(Gcc)和调试工具(Gdb)....在ubuntu新建一个c++文件: nano demo.cc或者demo.cpp 示例文件 demo.cc 的源代码如下: #include using namespace std; int sum(int, int)
  • 一:Linux下安装与配置MPICH为了在Linux下运行我们的并行程序,我们需要安装MPICH,现在的版本是MPICH2,下载地址:mpich2-1.5.tar.gz [文件大小:21.77 MB] 下载:用户名:www.linuxidc.com密码:www.muu.cc在 2012...
  • LINUX下gcc和cc的区别

    2014-04-18 16:21:01
    从名字上看,老的unix系统的CC程序叫做C Compiler。但GCC这个名字按GNU的说法叫做Gnu Compiler Collection。因为gcc包含很多编译器(C, C++, Objective-C, Ada, Fortran,and Java)。所以它们是不一样的,一个是一个...
  • 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...
  • Linux下分析 C程序的性能

    千次阅读 2014-08-15 13:37:34
    设置编译选项: SConstruct中添加: ...env=Environment(CC='gcc',  CFLAGS=['-g','-pg'],  LINKFLAGS=['-pg'],  LIBPATH=['/usr/local/lib','/lib64'],  CPPPATH=['.','/usr/local/include','/usr/includ
  • // system_exe.cc #include <cassert> #include <spdlog/common.h> #include <spdlog/spdlog.h> #include <string> #include <unistd.h> int main(int argc, char *argv[]) { ...
  • Linux下如何编译并运行C程序

    千次阅读 2015-04-16 19:11:16
    综合自:http://www.cnblogs.com/javadotnet/articles/1918930.html  ... CCLinux操作系统一个非常重要的源代码编译工具,有着许多重要的选项,支持许多不同语言的编译,如C、C++、Ada、Fortran、Objecti
  • 传统在Linux下开发程序,是在文字模式下,利用vi等文字编辑器撰写C/C++程序存盘后,在Command line下使用gcc编译,若要debug,则使用gdb。这种开发方式生产力并不高,若只是开发学习用的小程序则影响不大,但若要...
  • "cc-by-nc-nd-4.0""本文从编译、二进制程序文件和运行角度逐级解析了 Linux C 语言程序中几种变量类型"吴章金老师《360度剖析Linux ELF系列文章》:背景说明前几天,有同学在 “泰晓原创团队” 讨论群问道:请教,...
  • 设计一个游戏快捷键app,使用键盘和串口进行组合,制定几个特殊的按键作为游戏快捷键,按时通过串口输出相应的功能信息。可通过串口发送指令对各快捷键的功能进行重定义。并尝试使用Makefile文件实现代码的自动...
  • 记录一下Ubuntu16.04写的Qt程序,调试时没问题,运行时偶现崩溃需要在运行时生成core dump文件首先在pro结尾里加入QMAKE_CC += -gQMAKE_CXX += -gQMAKE_LINK += -g在终端输入 ulimit -c 显示为 0然后输入 ulimit -...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 433
精华内容 173
关键字:

linux下cc程序

linux 订阅