linux_gcc,gdb 使用

GCC

编译单个源文件

# gcc -o hello hello.c

# ./hello

在默认情况下产生的可执行程序名为hello.out,但你通常可以通过gcc的“-o”选项来指定自己的可执行程序名称。

编译多个源文件

源文件:say.c

#include <stdio.h> 
void sayHello() 

  printf(“Hello, world!\n”); 
}

使用gcc的“-c”标记来编译支持库代码:

# gcc -c say.c

这一过程的输出结果是一个名为say.o的文件,它包含适合连接到一个较大程序的已编译目标代码。

main函数,源文件main.c

   #include <stdio.h>  

    void sayHello();      

    int main()  

    {  

           sayHello();

    }  

现在有了两个目标文件:say.omain.o。它们包含能够被Linux执行的目标代码,要从这个目标代码创建Linux可执行程序,需要再一次调用GCC来执行连接阶段的工作:

# gcc -o hello.out say.o main.o

# ./hello.out

前面这些单独的步骤也可以简化为一个命令,这是因为GCC对如何将多个源文件编译为一个可执行程序有内置的规则。

# gcc -o hello.out say.c main.c

Gdb调试

首先安装gdb: yum install gdb

# gcc -g -o demo test.c

注意:

  • Gdb进行调试的是可执行文件而不是“.c”源文件因此需要先通过Gcc编译生成可执行文件才能用Gdb进行调试.
  • 一定要加上选项“-g”, 这样编译出的可执行代码中才包含调试信息否则Gdb无法载入该可执行文件.
  • 不能使用 -O2选项对可执行文件进行优化因为优化之后可执行文件里的符号表信息将被删除这样Gdb就无法找到使可执行文件与源文件之间的关联了也就不能调试了.

(1) 启动Gdb

$ gdb demo

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

(2) 查看源文件 在Gdb中键入“l”(list的缩写)可以查看所载入的文件

(gdb) l

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

(3) 设置断点 设置断点是调试程序中一个非常重要的手段它可以使程序到一定位置暂停运行因此,可以在该位置方便地查看变量的值堆栈情况等从而找出代码的症结所在Gdb中设置断点非常简单只需在“b”后加入对应的行号即可(这是最常用的方式). 如下所示:

(gdb) b 9

注意该断点的作用是当程序运行到第 行时暂停(第 行执行完毕第 行未执行

(4) 查看断点信息

(gdb) info b

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

(gdb) r

(6) 查看变量值 键入p(print的缩写)+变量名即可查看该变量在此时的值

(gdb) p a

(gdb) p b

(7) 单步执行 单步运行可以使用n(next的缩写)或者s(step的缩写), 它们之间的区别在于若有函数调用的时候, s会进入该函数而n不会因此, s就类似于VC等工具中的“step in”, n就类似于VC等工具中的“step over”. 执行 命令时进入函数内部如果用 命令则跳过函数的调用部分

(8) 恢复程序运行 在查看变量值以及堆栈之后就可以使用命令c(continue)恢复程序的正常运行了这时它会把剩余还未执行的程序执行完并显示剩余程序的执行结果.

(gdb) c

程中常会用到的命令介绍:
list :显示程序中的代码,常用使用格式有:
list
输出从上次调用list命令开始往后的10行程序代码。
list -
输出从上次调用list命令开始往前的10行程序代码。
list n
输出第n行附近的10行程序代码。
list function
输出函数function前后的10行程序代码。
forward/search :从当前行向后查找匹配某个字符串的程序行。使用格式:forward/search 字符串
查找到的行号将保存在$_变量中,可以用print $_命令来查看。
reverse-search :和forward/search相反,向前查找字符串。使用格式同上。
break :在程序中设置断点,当程序运行到指定行上时,会暂停执行。使用格式:break 要设置断点的行号
tbreak :设置临时断点,在设置之后只起作用一次。使用格式:tbreak 要设置临时断点的行号
clear :和break相反,clear用于清除断点。使用格式:clear 要清除的断点所在的行号
run :启动程序,在run后面带上参数可以传递给正在调试的程序。
awatch :用来增加一个观察点(add watch),使用格式:awatch 变量或表达式
当表达式的值发生改变或表达式的值被读取时,程序就会停止运行。
watch :与awatch类似用来设置观察点,但程序只有当表达式的值发生改变时才会停止运行。使用格 式:watch 变量或表达式
需要注意的是,awatchwatch都必须在程序运行的过程中设置观察点,即可运行run之后才能设置。
commands :设置在遇到断点后执行特定的指令。使用格式有:

commands设置遇到最后一个遇到的断点时要执行的命令
commands n 设置遇到断点号n时要执行的命令
注意,commands后面跟的是断点号,而不是断点所在的行号。
在输入命令后,就可以输入遇到断点后要执行的命令,每行一条命令,在输入最后一条命令后输入end就可以结束输入。
delete :清除断点或自动显示的表达式。使用格式:delete 断点号
disable :让指定断点失效。使用格式:disable 断点号列表       断点号之间用空格间隔开。
enable :和disable相反,恢复失效的断点。使用格式:enable 断点编号列表
ignore :忽略断点。使用格式:ignore 断点号 忽略次数
condition :设置断点在一定条件下才能生效。使用格式:condition 断点号 条件表达式
cont/continue :使程序在暂停在断点之后继续运行。使用格式:

     cont 跳过当前断点继续运行。

     cont n 跳过n次断点,继续运行。
n1时,cont 1即为cont
jump :让程序跳到指定行开始调试。使用格式:jump 行号
next :继续执行语句,但是跳过子程序的调用。使用格式:
     next 执行一条语句
     next n 执行n条语句
nexti :单步执行语句,但和next不同的是,它会跟踪到子程序的内部,但不打印出子程序内部的语句。使用格式同上。
step :与next类似,但是它会跟踪到子程序的内部,而且会显示子程序内部的执行情况。使用格式同上。
stepi :与step类似,但是比step更详细,是nextistep的结合。使用格式同上。

*step是进入函数,而  跳出函数--finish(函数执行完毕)or return(后面的代码不再执行) - 跳出循环until + 循环外行号

whatis :显示某个变量或表达式的数据类型。使用格式:whatis 变量或表达式
ptype :和whatis类似,用于显示数据类型,但是它还可以显示typedef定义的类型等。使用格式:ptype 变量或表达式
设置程序中变量的值。使用格式:
set 变量=表达式
set 变量:=表达式

p 变量=表达式
display :增加要显示值的表达式。使用格式:display 表达式
info display :显示当前所有的要显示值的表达式。
delete display/undisplay :删除要显示值的表达式。使用格式:delete display/undisplay 表达式编号
disable display :暂时不显示一个要表达式的值。使用格式:disable display 表达式编号
enable display :与disable display相反,使用表达式恢复显示。使用格式:enable display 表达式编号
* print :打印变量或表达式的值。使用格式:print 变量或表达式  或者p 变量或表达式
表达式中有两个符号有特殊含义:$$$
$表示给定序号的前一个序号,$$表示给定序号的前两个序号。
如果$$$后面不带数字,则给定序号为当前序号。
backtrace :打印指定个数的栈帧(stack frame)。使用格式:backtrace 栈帧个数
frame :打印栈帧。使用格式:frame 栈帧号
info frame :显示当前栈帧的详细信息。
select-frame :选择栈帧,选择后可以用info frame来显示栈帧信息。使用格式:select-frame 栈帧号
* kill :结束当前程序的调试。
quit :退出gdb

如要查看所有的gdb命令,可以在gdb下键入两次Tab(制表符),运行“help command”可以查看命令command的详细使用格式。

暂停点

暂停 / 恢复程序运行

调试程序中,暂停程序运行是必须的,GDB可以方便地暂停程序的运行。你可以设置程序的在哪行停住,在什么条件下停住,在收到什么信号时停往等等。以便于你查看运行时的变量,以及运行时的流程。

当进程被gdb停住时,你可以使用info program 来查看程序的是否在运行,进程号,被暂停的原因。

在gdb中,我们可以有以下几种暂停方式:断点(BreakPoint)、观察点(WatchPoint)、捕捉点(CatchPoint)、信号(Signals)、线程停止(Thread Stops)。如果要恢复程序运行,可以使用c或是continue命令。

设置断点(BreakPoint)

我们用break命令来设置断点。正面有几点设置断点的方法:

break <function>

在进入指定函数时停住。C++中可以使用class::function或function(type,type)格式来指定函数名

break <linenum>

在指定行号停住

break +offset

break -offset

在当前行号的前面或后面的offset行停住。offset为自然数

break filename:linenum

在源文件filename的linenum行处停住

break filename:function

在源文件filename的function函数的入口处停住

break *address

在程序运行的内存地址处停住

break

break命令没有参数时,表示在下一条指令处停住

break ... if <condition>

...可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环境体中,可以设置break if i=100,表示当i为100时停住程序

info breakpoints [n]

info break [n]

查看断点时,可使用info命令,如下所示:(注:n表示断点号)

设置观察点(WatchPoint)

    观察点一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。我们有下面的几种方法来设置观察点:    
    watch <expr>    为表达式(变量)expr设置一个观察点。一量表达式值有变化时,马上停住程序。        
    rwatch <expr>   当表达式(变量)expr被读时,停住程序。        
    awatch <expr>   当表达式(变量)的值被读或被写时,停住程序。    
    info watchpoints 列出当前所设置了的所有观察点。

设置捕捉点(CatchPoint)

你可设置捕捉点来捕捉程序运行时的一些事件。如:载入共享库(动态链接库)或是C++的异常。设置捕捉点的格式为:

catch <event> 和tcatch <event> ,用help catch和help tcatch来看具体用法。

维护停止点

上面说了如何设置程序的停止点,GDB中的停止点也就是上述的三类。在GDB中,如果你觉得已定义好的停止点没有用了,你可以使用delete、clear、disable、enable这几个命令来进行维护。

    clear         清除所有的已定义的停止点。

clear <function>,clear <filename:function>        清除所有设置在函数上的停止点。

    clear <linenum>,clear <filename:linenum>        清除所有设置在指定行上的停止点。

    delete [breakpoints] [range...]        删除指定的断点,breakpoints为断点号。如果不指定断点号,则表示删除所有的断点。range 表示断点号的范围(如:3-7)。其简写命令为d。
比删除更好的方法是disable停止点,disable了的停止点,GDB不会删除,当你还需要时,enable即可,就好像回收站一样。

    disable [breakpoints] [range...]
        disable所指定的停止点,breakpoints为停止点号。如果什么都不指定,表示disable所有的停止点。简写命令是dis.

    enable [breakpoints] [range...]
        enable所指定的停止点,breakpoints为停止点号。

    enable [breakpoints] once range...
        enable所指定的停止点一次,当程序停止后,该停止点马上被GDB自动disable。

    enable [breakpoints] delete range...
        enable所指定的停止点一次,当程序停止后,该停止点马上被GDB自动删除。


用GDB调试程序

GDB概述
————

GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长,尺有所短”就是这个道理。

一般来说,GDB主要帮忙你完成下面四个方面的功能:

    1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
    2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
    3、当程序被停住时,可以检查此时你的程序中所发生的事。
    4、动态的改变你程序的执行环境。

从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。


一个调试示例
——————

源程序:tst.c

     1 #include <stdio.h>
     2
     3 int func(int n)
     4 {
     5         int sum=0,i;
     6         for(i=0; i<n; i++)
     7         {
     8                 sum+=i;
     9         }
    10         return sum;
    11 }
    12
    13
    14 main()
    15 {
    16         int i;
    17         long result = 0;
    18         for(i=1; i<=100; i++)
    19         {
    20                 result += i;
    21         }
    22
    23        printf("result[1-100] = %d /n", result );
    24        printf("result[1-250] = %d /n", func(250) );
    25 }

编译生成执行文件:(Linux下)
    hchen/test> cc -g tst.c -o tst

使用GDB调试:

hchen/test> gdb tst  <---------- 启动GDB
GNU gdb 5.1.1
Copyright 2002 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-suse-linux"...
(gdb) l     <-------------------- l命令相当于list,从第一行开始例出原码。
1        #include <stdio.h>
2
3        int func(int n)
4        {
5                int sum=0,i;
6                for(i=0; i<n; i++)
7                {
8                        sum+=i;
9                }
10               return sum;
(gdb)       <-------------------- 直接回车表示,重复上一次命令
11       }
12
13
14       main()
15       {
16               int i;
17               long result = 0;
18               for(i=1; i<=100; i++)
19               {
20                       result += i;    
(gdb) break 16    <-------------------- 设置断点,在源程序第16行处。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func  <-------------------- 设置断点,在函数func()入口处。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break  <-------------------- 查看断点信息。
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x08048496 in main at tst.c:16
2   breakpoint     keep y   0x08048456 in func at tst.c:5
(gdb) r           <--------------------- 运行程序,run命令简写
Starting program: /home/hchen/test/tst

Breakpoint 1, main () at tst.c:17    <---------- 在断点处停住。
17               long result = 0;
(gdb) n          <--------------------- 单条语句执行,next命令简写。
18               for(i=1; i<=100; i++)
(gdb) n
20                       result += i;
(gdb) n
18               for(i=1; i<=100; i++)
(gdb) n
20                       result += i;
(gdb) c          <--------------------- 继续运行程序,continue命令简写。
Continuing.
result[1-100] = 5050       <----------程序输出。

Breakpoint 2, func (n=250) at tst.c:5
5                int sum=0,i;
(gdb) n
6                for(i=1; i<=n; i++)
(gdb) p i        <--------------------- 打印变量i的值,print命令简写。
$1 = 134513808
(gdb) n
8                        sum+=i;
(gdb) n
6                for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8                        sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6                for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt        <--------------------- 查看函数堆栈。
#0  func (n=250) at tst.c:5
#1  0x080484e4 in main () at tst.c:24
#2  0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish    <--------------------- 退出函数。
Run till exit from #0  func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24              printf("result[1-250] = %d /n", func(250) );
Value returned is $6 = 31375
(gdb) c     <--------------------- 继续运行。
Continuing.
result[1-250] = 31375    <----------程序输出。

Program exited with code 027. <--------程序退出,调试结束。
(gdb) q     <--------------------- 退出gdb。
hchen/test>

好了,有了以上的感性认识,还是让我们来系统地认识一下gdb吧。

 


使用GDB
————

一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如:

    > cc -g hello.c -o hello
    > g++ -g hello.cpp -o hello

如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。当你用-g把调试信息加入之后,并成功编译目标代码以后,让我们来看看如何用gdb来调试他。

启动GDB的方法有以下几种:

    1、gdb <program> 
       program也就是你的执行文件,一般在当然目录下。

    2、gdb <program> core
       用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件。

    3、gdb <program> <PID>
       如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

 

GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb -help查看。我在下面只例举一些比较常用的参数:

    -symbols <file> 
    -s <file> 
    从指定文件中读取符号表。

    -se file 
    从指定文件中读取符号表信息,并把他用在可执行文件中。

    -core <file>
    -c <file> 
    调试时core dump的core文件。

    -directory <directory>
    -d <directory>
    加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。

1. gdb exe
    使得exe程序运行在debug环境下
2. break functiona
    在functiona函数处设置端点 
3. run
    让程序从main入口执行到断点functiona
4. n
    next,单步执行,相当于VC中的调试命令step over
5. s
    step into,进入子函数,察看子函数的执行情况

6. bt
    backtrace查看堆栈的情况

7. p variant
    print出变量variant的值
8. l
    list命令,查看当前的行的上下文,默认显示10行

9. p variant=correct value         
    如果发现此时的variant的值不正确,我们可以给variant设置一个正确的值(correct value)
    然后,用第10项中的命令继续执行

10. c
    continue 继续执行,可以是经过按照更改后的值继续执行。相当于VC中的F5

11. quit or Ctrl+C
     退出gdb
    
在gdb的命令行下,可以通过file exeprogram 载入要debug的文件
gdb -silent 表示不提示GDB的版权信息 or gdb -q (quiet)
gdb -h      显示gdb的帮助

12 About Help
gdb>help

apropos args //查找所有的GDB命令以及它的文档中包含args的表达式
complete i    //列出所有以i开头的gdb命令
针对某一个命令的帮助是help command ,例如help info
显示info的用法,info 可以查看args,breakpoints,stack......
show命令只要是显示gdb的信息,如show version

13   break
    break function
    在某一个函数的地方设置端点
    break linenum
    在确定的某一行的地方设置断点 
    break +offset
          -offset 
   
    break *address在某一个地址设置断点
         
14 watch
    watch expr
    查看某一个表达式
    rwatch expr
    查看某一个表达式,并在读入该表达式的时候,设置断点
   

15   查看源代码
    list lineNum   在lineNum的前后源代码显示出来
    list +   列出当前行的后面代码行
    list -   列出当前行的前面代码行
    list function
    set listsize count 
        设置显示代码的行数
    show listsize
         显示打印代码的行数
    list first,last
         显示从first到last的源代码行

16   编辑源代码
     edit   编辑当前所在的行
     edit num
     edit function 编辑包含函数定义的文件
     edit filename:function
   设置编辑器
     EDITOR=/usr/bin/vi
     export EDITOR
     gdb ....
转载:
GDB的使用 

    当程序出错并产生core 时 
    快速定位出错函数的办法 
    gdb 程序名 core文件名(一般是core,也可能是core.xxxx) 
     
    调试程序使用的键 
      r   run 运行.程序还没有运行前使用 
      c   cuntinue  继续运行。运行中断后继续运行 
      q   退出 
      kill 终止调试的程序 
      h   help  帮助 
      <tab>; 命令补全功能 
       
      step 跟入函数 
      next 不跟入函数 
      b   breakpoint 设置断点。 
          用法: 
          b  函数名  对此函数进行中断 
          b  文件名:行号 对此文件中指定行中断.如果是当前文件,那么文件名与:号可以 
省略 
  看当前断点数使用info break.禁止断点disable 断点号.删除delete  断点号. 

l   list 列出代码行。一次列10 行。连接使用list将会滚动显示. 也可以在list 后面 
跟上 文件名:行号 
      watch 观察一个变量的值。每次中断时都会显示这个变量的值 
      p   print 打印一个变量的值。与watch不同的是print只显示一次 
         这里在顺便说说如何改变一个 value. 当你下指令 p 的时候,例如你用 p b, 
          这时候你会看到 b 的 value, 也就是上面的 $1 = 15.  
          你也同样可以用 p 来改变一个 value, 例如下指令 p b = 100 试试看, 
          这时候你会发现, b 的 value 就变成 100 了:$1 = 100.  

网上抄录 
基本的使用方法简介 
前言 
程序代码中的错误可分为数类,除了最容易除错的语法错误,编译程序会告诉你错误所在外,大部分的错误都可以归类为执行时错误。GDB 的功能便是寻找执行时错误。如果没有除错程序,我们只能在程序中加入输出变量值的指令来了解程序执行的状态。有了 GDB 除错程序,我们可以设定在任何地方停止程序的执行,然后可以随意检视变量值及更动变量,并逐行执行程序。  
一个除错程序执行的流程通常是这样的:  
   
1. 进入除错程序并指定可执行文件。  
2. 指定程序代码所在目录。  
3. 设定断点后执行程序。  
4. 程序于断点中断后,可以 (1)检视程序执行状态;检视变量值或变更变量值 (2) 逐步执行程序,或是全速执行程序到下一个断点或是到程序结束为止。  
5. 离开除错程序。  
以下将分为下列数项分别介绍:  
1. 进入 GDB 及指定可执行档  
2. 指定程序代码所在目录及检视程序代码  
3. 断点的设定与清除  
4. 全速及逐步执行程序  
5. 检视及更改变量值  
6. 检视程序执行状态  
7. 读取 Core 文件信息  
进入 GDB 及指定可执行档: 
1. 进入 GDB 并读入可执行档 (档名为 'PROGRAM'),准备进行除错。  
gdb PROGRAM 
指定程序代码所在目录及检视程序代码 
1. 增加目录 DIR 到收寻程序代码的目录列表 (如果你的程序代码和可执行档放在同一个目录下,就不须指定程序代码所在目录。):  
(gdb) directory DIR  
   
2. 检视程序代码,格式计有:  
(gdb) list   =>;  显示目前执行程序代码前后各五行的程序代码;或是显示从上次 list 之后的程序代码  
(gdb) list function   =>;  显示该程序开始处前后五行的程序代码。  
(gdb) list -               =>;上次显示程序代码的前面的十行。 
断点的设定与清除 
1. 设定断点(指令为 break,可简写为 (b),格式计有:  
(gdb) break filename.c:30   =>; 在 filename.c 的第三十行处停止执行。  
(gdb) break function           =>; 在进入 function 时中断程序的执行。  
(gdb) break filename.c:function   =>; 在程序代码档 filename.c 中的函数 function 处设定断点。  
(gdb) break  =>;  在下一个将被执行的命令设定断点。  
(gdb) break ... if cond  =>; 只有当 cond 成立的时候才中断。cond 须以 C 语言的语法写成。  
   
2. 显示各个断点的信息。  
(gdb) info break  
   
3. 清除断点(命令为 clear),格式同 break 。例如 :  
(gdb) clear filename.c:30  
   
4. 清除断点,NUM 是在 info break 显示出来的断点编号。  
(gdb) delete NUM 
全速及逐步执行程序 
1. 从程序开头全速执行程序,直到遇到断点或是程序执行完毕为止。  
(gdb) run  
   
2. 在程序被中断后,全速执行程序到下一个断点或是程序结束为止 (continue 指令可简写为 c)。  
(gdb) continue  
   
3. 执行一行程序. 若呼叫函数, 则将该包含该函数程序代码视为一行程序 (next 指令可简写为 n)。  
(gdb) next  
   
4. 执行一行程序. 若呼叫函数, 则进入函数逐行执行 (step 指令可简写为 s)。  
(gdb) step  
   
5. 执行一行程序,若此时程序是在 for/while/do loop 循环的最后一行,则一直执行到循环结束后的第一行程序后停止 (until 指令可简写为 u)。  
(gdb) until  
   
6. 执行现行程序到回到上一层程序为止。  
(gdb) finish 
检视及更改变量值 
1. print 叙述,显示该叙述执行的结果 (print 指令可简写为 p)。如  
(gdb) print a              =>; 显示 a 变量的内容.  
(gdb) print sizeof(a)  =>; 显示 a 变量的长度.  
   
2. display 叙述,在每个断点或是每执行一步时显示该叙述值。如  
 (gdb) display a  
   
3. 更改变量值:  
(gdb) print (a=10)    =>; 将变量 a 的值设定为 10.  
  
检视程序执行状态 
1. 查看程序执行到此时,是经过哪些函数呼叫的程序 (backtrace 指令可简写为 bt),也就是查看函数呼叫堆栈。  
(gdb) backtrace 
读取 Core 文件信息 
1. 读入 PROGRAM 及 PROGRAM.CORE 档,可检视 Core Dump 时程序变量值及程序流程状态 。  
gdb  PROGRAM  core  
说明:'core' 档案是由 PROGRAM 档执行后,遇到 Core Dump 时产生的 Core 檔檔名。如果你还需要该 Core 档,我们建议你将该档案档名更改为 PROGRAM.core。在输入上述命令后,你可以用 GDB 提供的检视变量值以及检视程序执行状态来读取程序 Core Dump 时的状态。

平时在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等解释语言也会经历一个词法分析和语法分析的阶段,不过之后并不会进行“翻译”,而是“解释”,边解释边执行
    ************************

A、解释程序 

所谓解释程序是高级语言翻译程序的一种,它将源语言(如BASIC)书写的源程序作为输入,解释一句后就提交计算机执行一句,并不形成目标程序。就像外语翻译中的“口译”一样,说一句翻一句,不产生全文的翻译文本。这种工作方式非常适合于人通过终端设备与计算机会话,如在终端上打一条命令或语句,解释程序就立即将此语句解释成一条或几条指令并提交硬件立即执行且将执行结果反映到终端,从终端把命令打入后,就能立即得到计算结果。这的确是很方便的,很适合于一些小型机的计算问题。但解释程序执行速度很慢,例如源程序中出现循环,则解释程序也重复地解释并提交执行这一组语句,这就造成很大浪费。 

B、编译程序 

这是一类很重要的语言处理程序,它把高级语言(如FORTRAN、COBOL、Pascal、C等)源程序作为输入,进行翻译转换,产生出机器语言的目标程序,然后再让计算机去执行这个目标程序,得到计算结果。 

编译程序工作时,先分析,后综合,从而得到目标程序。所谓分析,是指词法分析和语法分析;所谓综合是指代码优化,存储分配和代码生成。为了完成这些分析综合任务,编译程序采用对源程序进行多次扫描的办法,每次扫描集中完成一项或几项任务,也有一项任务分散到几次扫描去完成的。下面举一个四遍扫描的例子:第一遍扫描做词法分析;第二遍扫描做语法分析;第三遍扫描做代码优化和存储分配;第四遍扫描做代码生成。 

值得一提的是,大多数的编译程序直接产生机器语言的目标代码,形成可执行的目标文件,但也有的编译程序则先产生汇编语言一级的符号代码文件,然后再调用汇编程序进行翻译加工处理,最后产生可执行的机器语言目标文件。 

在实际应用中,对于需要经常使用的有大量计算的大型题目,采用招待速度较快的编译型的高级语言较好,虽然编译过程本身较为复杂,但一旦形成目标文件,以后可多次使用。相反,对于小型题目或计算简单不太费机时的题目,则多选用解释型的会话式高级语言,如BASIC,这样可以大大缩短编程及调试的时间

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

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

$ 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,以便根据不同的运行平台和用户要求产生经过优化的汇编代码。例如,

$ 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为例。

$ 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

 

 

 

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/http://www.cnblogs.com/../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/../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/http://www.cnblogs.com/http://www.cnblogs.com/i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/http://www.cnblogs.com/.. 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/http://www.cnblogs.com/../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中并没有这个节区,所以不关心它)。

 

 

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!

 

 

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'

 

 

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

 

 

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

 

 

Quote:

$ ./test
hello, world!
Segmentation fault




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值