调试程序中,暂停程序运行是必须的,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 |
语法错误是程序开发过程中难以避免的错误(人的大脑在很多条件下都容易开小差),不过编译器往往能够通过语法检查快速发现这些错误,并准确地告诉你语法错误的大概位置。因此,作为开发人员,要做的事情不是“恐慌”(不知所措),而是认真阅读编译器的提示,根据平时积累的经验(最好在大脑中存一份常见语法错误索引,很多资料都提供了常见语法错误列表,如<C Traps&Pitfalls>和最后面的参考资料[12]也列出了很多常见问题)和编辑器提供的语法检查功能(语法加亮、括号匹配提示等)快速定位语法出错的位置并进行修改。
语法检查之后就是翻译动作,gcc提供了一个优化选项-O,以便根据不同的运行平台和用户要求产生经过优化的汇编代码。例如,
$ gcc -o hello hello.c #采用默认选项,不优化 |
根据上面的简单演示,可以看出gcc有很多不同的优化选项,主要看用户的需求了,目标代码的大小和效率之间貌似存在一个“纠缠”,需要开发人员自己权衡。
下面我们通过-S选项来看看编译出来的中间结果,汇编语言,还是以之前那个hello.c为例。
$ gcc -S hello.c #默认输出是hello.s,可自己指定,输出到屏幕-o -,输出到其他文件-o file |
不知道看出来没?和我们在课堂里学的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 |
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) |
经过上面的演示基本可以看出它们之间的不同。可重定位文件本身不可以运行,仅仅是作为可执行文件、静态连接库(也是可重定位文件)、动态连接库的 “组件”。静态连接库和动态连接库本身也不可以执行,作为可执行文件的“组件”,它们两者也不同,前者也是可重定位文件(只不过可能是多个可重定位文件的集合),并且在连接时加入到可执行文件中去;而动态连接库在连接时,库文件本身并没有添加到可执行文件中,只是在可执行文件中加入了该库的名字等信息,以便在可执行文件运行过程中引用库中的函数时由动态连接器去查找相关函数的地址,并调用它们。从这个意义上说,动态连接库本身也具有可重定位的特征,含有可重定位的信息。对于什么是重定位?如何进行静态符号和动态符号的重定位,我们将在链接部分和《动态符号链接的细节》一节介绍。
下面来看看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 |
从上表可以看出,对于可重定位文件,会包含这些基本节区.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。为了进一步理解这些节区和源代码的关系,这里来看一看myprintf.c产生的汇编代码。
Quote: |
$ gcc -S myprintf.c |
是不是可以从中看出可重定位文件中的那些节区和汇编语言代码之间的关系?在上面的可重定位文件,可以看到有一个可重定位的节区,即. 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的节区表也列出 |
上表给出了可执行文件的如下几个段(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 #把可重定位文件链接成可执行文件 |
从上边的演示看出,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 \ |
不出我们所料,它完美的运行了。下面通过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参数 |
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 |
Quote: |
$ readelf -s /usr/lib/crt1.o | grep __libc_csu_init |
Quote: |
$ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc |
Quote: |
$ ./test |