上一篇介绍的GNU调试器是一个非常灵活的工具,但是它的用户界面还有许多需要改进的地方。所以,通常想使用gdb调试大型的应用程序就很困难了。为了解决这个问题,就有了几款gdb的图形界面的应用程序,其中一个用的比较流行的程序是KDE调试器即kdbg,它是由Johannes Sixt创建的...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第91页到103页(注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为103 / 577)。

The KDE Debugger (KDE调试器):

    上一篇介绍的GNU调试器(gdb)是一个非常灵活的工具,但是它的用户界面还有许多需要改进的地方。所以,通常想使用gdb调试大型的应用程序就很困难了。为了解决这个问题,就有了几款gdb的图形界面的应用程序,其中一个用的比较流行的程序是KDE调试器即kdbg,它是由Johannes Sixt创建的。

    kdbg软件包依赖于K桌面环境即KDE,KDE是一种在许多unix系统下如linux中,被广泛使用的X视窗图形环境,它是由Qt图形库开发的,所以你的系统中必须已经安装好了Qt的运行时库。

Downloading and installing kdbg (下载安装kdbg调试器):

    许多linux发行版将kdbg软件包作为额外的软件包,所以默认并没安装,对于英文原著作者所使用的Mandrake系统,kdbg软件包被放在附加的程序盘中,如果你的linux系统没有该软件包,你可以进入kdbg的官网:http://www.kdbg.org/ ,下载源代码进行编译安装,在译者翻译的时候,最新稳定的发布版本是2.5.3的版本。

    小提示:编译kdbg的源代码需要KDE的开发头文件,它被包含在KDE的开发软件包中,该软件包多数情况下会随KDE一起被安装好。

Using kdbg (kdbg的使用):

    安装好kdbg后,可以在命令行下输入kdbg命令来启动它,kdbg启动后,可以使用文件菜单或工具栏对应的图标来选择要调试的可执行文件以及原始的源代码文件。

    一旦可执行文件和源代码文件被加载后,你就可以开始调试会话了。因为kdbg是gdb的图形界面,所以可以使用和gdb一样的命令,你可以选择需要调试的源代码行,然后使用工具栏的停止符号来设置断点,如下图所示:


图1

    如果你想查看程序执行过程中内存或寄存器的值,你可以点击View菜单,然后选择你需要查看的窗口。你还可以打开一个输出窗口来查看程序执行过程中输出的信息,下图显示了寄存器窗口的例子:
 

图2

    一切准备好后,你就可以选择运行按钮来运行程序,程序会在你设置的断点处停下来,此时就可以通过单步执行按钮来进行一步步的调试。

The GNU Objdump Program (GNU的Objdump程序):

    GNU的objdump程序是binutils软件包中的工具之一,这对于程序员来说也是一个非常有用的工具,通过objdump程序你既可以查看编译生成的目标代码文件中的汇编格式的代码,也可以查看原始的指令集格式的代码。

    下面就介绍objdump程序的使用方法。

Using objdump (objdump的使用):

    objdump的命令行格式如下:

objdump [-a|--archive-headers] [-b bfdname|--target=bfdname]
    [-C|--demangle[=style] ] [-d|--disassemble]
    [-D|--disassemble-all] [-z|--disassemble-zeroes]
    [-EB|-EL|--endian={big | little }] [-f|--file-headers]
    [--file-start-context] [-g|--debugging]
    [-e|--debugging-tags] [-h|--section-headers|--headers]
    [-i|--info] [-j section|--section=section]
    [-l|--line-numbers] [-S|--source]
    [-m machine|--architecture=machine]
    [-M options|--disassembler-options=options]
    [-p|--private-headers] [-r|--reloc]
    [-R|--dynamic-reloc] [-s|--full-contents]
    [-G|--stabs] [-t|--syms] [-T|--dynamic-syms]
    [-x|--all-headers] [-w|--wide]
    [--start-address=address] [--stop-address=address]
    [--prefix-addresses] [--[no-]show-raw-insn]
    [--adjust-vma=offset] [-V|--version] [-H|--help]
    objfile...

    这些命令行参数的作用如下表所示:

Parameter Description
-a If any files are archives, display the archive 
header information 
如果文件是库归档文件,则显示出归档文件的
头部信息,即列举出归档里的文件列表 
-b Specify the object code format of the 
object code files 
指定目标代码文件的目标代码格式
-C Demangle low-level symbols into 
user-level names 
将低级的符号信息解码为用户可读的名字,
这让C++之类的函数名变得可读
-d Disassemble the object code into 
instruction code 
将目标代码反汇编为指令代码格式
-D Disassemble all sections into instruction code, 
including data 
将所有段,包括数据段,都反汇编为指令代码格式
-EB Specify big-endian object files 
指定目标文件为大字节序 
-EL Specify little-endian object files 
指定目标文件为小字节序
-f Display summary information from the 
header of each file 
从每个文件的整体头部信息中显示出摘要信息
-G Display the contents of the debug sections 
显示出调试段的内容
-h Display summary information from the 
section headers of each file 
从每个文件的段的头部信息中显示出摘要信息 
-i Display lists showing all architectures and 
object formats 
以列表形式显示出所有可用的架构和目标格式
-j Display information only for the specified section 
显示出指定段的信息
-l Label the output with source code line numbers 
在输出中标记上源代码的行号信息,
只能配合-d,-D及-r参数一起使用
-m Specify the architecture to use when disassembling 
在反汇编时,指定所使用的架构
-p Display information specific to the object file format 
显示出和目标代码文件格式相特定的信息
-r Display the relocation entries in the file 
显示出文件中的重定位项目
-R Display the dynamic relocation entries in the file 
显示出文件中的动态重定位项目
-s Display the full contents of the specified sections 
显示出指定段的完整内容
-S Display source code intermixes with 
disassembled code 
将源代码和反汇编代码混合在一起进行输出显示
-t Display the symbol table entries of the files 
显示出文件中的符号表
-T Display the dynamic symbol table entries of the files 
显示出文件中的动态符号表
-x Display all available header information of the files 
显示出文件中所有可用的头信息
--start-address Start displaying data at the specified address 
在指定地址开始显示数据
--stop-address Stop displaying data at the specified address 
在指定地址结束显示数据

    objdump程序可以说是很有用的工具,它除了对编译生成的目标代码文件起作用外,还可以解码很多不同类型的二进制格式的文件。对于汇编程序员来说,最感兴趣的应该是-d参数,它可以显示出目标代码文件的反汇编输出。

An objdump example (objdump的例子):

    下面是对C程序生成的目标代码文件进行反汇编的例子:

$ gcc -c ctest.c
$ objdump -d ctest.o

ctest.o: file format elf32-i386

Disassembly of section .text:

00000000 <main>:
	 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 <main+0xf>
	13: 83 c4 10				add $0x10,%esp
	16: 83 ec 0c				sub $0xc,%esp
	19: 6a 00				push $0x0
	1b: e8 fc ff ff ff			call 1c <main+0x1c>
$

    具体的反汇编的代码情况是和你所使用的编译器的类型和版本等决定的,因为不同的编译器或者不同版本的编译器生成的汇编代码可能不会完全一致,有的目标代码优化过,有的没优化过等也都会影响最终生成的指令代码。上面的命令中先使用gcc加-c参数生成中间的目标代码文件,然后用objdump输出该文件里的反汇编信息,可以看到objdump将指令字节码和对应的汇编格式的代码都显示了出来。你可能已经注意到代码的内存地址是从0开始的,这些地址将在链接器生成可执行程序的过程中才能确定下来。通过objdump,你就可以很轻松的查看到程序编译生成的指令代码。

The GNU Profiler Program (GNU分析器):

    GNU分析器即gprof也是binutils软件包中的工具之一,该工具可以用于分析程序的执行性能,从而检测出程序的哪些地方占用过多的处理器时间,然后你就可以有针对性的进行程序的调整优化,下面就介绍下该工具的用法。

Using the profiler (分析器的使用):

    gprof也是一个命令行的工具,它的命令行格式如下:

gprof [ -[abcDhilLsTvwxyz] ] [ -[ACeEfFJnNOpPqQZ][name] ]
      [ -I dirs ] [ -d[num] ] [ -k from/to ]
      [ -m min-count ] [ -t table-length ]
      [ --[no-]annotated-source[=name] ]
      [ --[no-]exec-counts[=name] ]
      [ --[no-]flat-profile[=name] ] [ --[no-]graph[=name] ]
      [ --[no-]time=name] [ --all-lines ] [ --brief ]
      [ --debug[=level] ] [ --function-ordering ]
      [ --file-ordering ] [ --directory-path=dirs ]
      [ --display-unused-functions ] [ --file-format=name ]
      [ --file-info ] [ --help ] [ --line ] [ --min-count=n ]
      [ --no-static ] [ --print-path ] [ --separate-files ]
      [ --static-call-graph ] [ --sum ] [ --table-length=len ]
      [ --traditional ] [ --version ] [ --width=n ]
      [ --ignore-non-functions ] [ --demangle[=STYLE] ]
      [ --no-demangle ] [ image-file ] [ profile-file ... ]

    这些参数大致分为三类:
  • 输出格式参数
  • 统计分析参数
  • 其他的杂项参数
    输出格式参数如下表所示:

Parameter Description
-A Display source code for all functions, or 
just the functions specified 
显示出所有函数的源代码,或只显示指定函数的
源代码,必须在gcc编译时用-g参数生成过调试
符号信息才能显示出来
-b Don’t display verbose output explaining 
the analysis fields 
在输出的统计分析报表中会有大段的
注释说明信息来说明报表各个字段的含义,
可以通过-b参数,只显示报表的分析数据,
而不显示注释说明信息
-C Display a total tally of all functions, 
or only the functions specified 
显示出所有函数的执行次数信息,
或者只显示指定函数的执行次数信息
-i Display summary information about the 
profile data file 
显示出gmon.out这个包含统计信息的
数据文件里的摘要信息
-I Specifies a list of search directories 
to find source files 
指定源文件的搜索目录
-J Do not display annotated source code 
不显示注释的源代码
-L Display full pathnames of source filenames 
显示出源代码文件的完整路径名
-p Display a flat profile for all functions, 
or only the functions specified 
只输出统计分析报表中的flat profile部分,
该部分里包含了每个函数的执行时间,
调用次数,每次调用消耗的时间等信息
-P Do not print a flat profile for all functions, 
or only the functions specified 
不显示flat profile部分
-q Display the call graph analysis 
只输出统计分析报表中的call graph部分,
该部分显示了各个函数之间的调用关系,
某个函数在另一个函数里的调用所消耗的时间,
以及某个函数的子函数的总的执行时间等信息
-Q Do not display the call graph analysis 
不显示call graph部分
-y Generate annotated source code 
in separate output files 
在单独的输出文件中生成注释的源代码
-Z Do not display a total tally of functions 
and number of times called 
不显示函数的执行次数
-T Display output in traditional BSD style 
以传统的BSD样式来输出统计分析信息,
BSD格式下先输出注释说明信息,
再输出报表信息,和默认的顺序相反
-w Set the width of output lines 
设置输出行的宽度
--demangle C++ symbols are demangled when displaying output 
输出信息中解码C++的符号

    统计分析参数如下表所示:

Parameter Description
-a Does not analyze information about 
statistically declared (private) functions 
不分析静态声明的函数信息
-c Analyze information on child functions 
that were never called in the program 
分析程序中那些从没被调用过的子函数的信息
-D Ignore symbols that are not known to be 
functions (only on Solaris and HP OSs) 
忽略掉无法识别的函数符号
(仅用于Solaris和HP的系统中)
-k from/to Don't analyze functions matching a 
beginning and ending symspec 
不分析匹配from到to所代表的函数
-l Analyze the program by line instead of function 
使用行号代替函数名来分析程序
-m num This option affects execution count output only. 
Symbols that are executed less than num times 
are suppressed 
该选项只会影响执行次数的输出,
如果函数的执行次数小于-m参数指定的num值时,
该函数就会被禁止输出
-n Analyze only times for specified functions 
在call graph部分,仅分析指定函数的执行时间信息
-N Don't analyze times for the specified functions 
在call graph部分,不分析指定函数的执行时间信息
-z Analyze all functions, even those that were 
never called 
分析所有的函数,包括那些从没被调用过的函数

    最后就是其他的杂项参数:

Parameter Description
-d[num] The '-d num' option specifies debugging options. 
If num is not specified, enable all debugging 
该参数指定num对应的数字格式的调试级别,
如果没有指定num,则开启所有的调试
 
-O Specify the format of the profile data file 
指明gmon.out配置数据文件的格式
 
-s The '-s' option causes gprof to summarize 
the information in the profile data files it read in, 
and write out a profile data file called gmon.sum 
该参数会指示gprof去读取gmon.out配置数据文件
中的摘要信息,然后将摘要信息写入到一个
gmon.sum的文件中
 
-v Print the version of gprof 
打印出gprof的版本信息


    想对程序使用gprof,你必须确保你的程序在gcc编译时指定了-pg参数,使用该参数编译源代码时,会在程序的每个函数中插入一个名为mcount的函数,就是说-pg编译的应用程序里的每一个函数都会调用mcount, 而mcount会在内存中保存一张函数调用图,并通过函数调用堆栈的形式查找子函数和父函数的地址。这张调用图也保存了所有与函数相关的调用时间,调用次数等等的所有信息。程序运行结束后(编译后的程序必须至少运行一次才会有gmon.out),会在程序退出的路径下生成一个 gmon.out文件。这个文件就是记录并保存下来的监控数据。gprof通过解读gmon.out这个配置数据文件来对程序的性能进行分析。

    小提示:需要注意的是,每次运行程序,gmon.out文件的内容都会被覆盖掉,所以如果你想检测多个例子,那么可以将gmon.out文件改个名字,然后gprof测试的时候显式的指定该文件名即可,目前gcc编译后的程序运行时只能生成gmon.out这个文件名,所以只有用改文件名的方式。

    gprof输出主要包括两个统计报表:
  • flat profile部分,所有函数的执行时间和调用次数等的列表清单
  • call graph部分,所有函数的相互调用关系,函数在被调用函数中的执行时间和次数信息,及子函数的调用次数和时间信息等
    gprof默认是输出到终端的,你可以将输出重定向到某个文件中,将信息保存起来。

A profile example (gprof例子):

    首先,我们写一个测试用的C程序:

#include <stdio.h>
void function1()
{
    int i, j;
    for(i=0; i <100000; i++)
        j += i;
}
void function2()
{
    int i, j;
    function1();
    for(i=0; i < 200000; i++)
        j = i;
}
int main()
{
    int i, j;
    for (i = 0; i <100; i++)
        function1();
    for(i = 0; i<50000; i++)
        function2();
    return 0;
}

    上面程序的main入口函数会调用100次function1函数,还会调用50000次function2函数,而function2函数中每次又会调用一次function1函数。下面先用gcc对源代码进行编译:

$ gcc -o demo demo.c -pg
$ ./demo
$

    上面用gcc加-pg参数编译源代码,生成demo可执行程序,接着运行了一次demo程序,只有运行一次才会生成可供分析用的gmon.out配置数据文件。接下来我们用gprof来输出统计分析信息:

$ ls -al gmon.out
-rw-r--r-- 1 rich rich 426 Jul 7 12:39 gmon.out
$ gprof demo > gprof.txt
$

    上面的命令输出中可以看到gmon.out文件已经生成好了,接着gprof命令将输出重定向到gprof.txt文件中。下面是gprof.txt中的第一个部分flat profile的内容:

  %   cumulative    self           self    total
time   seconds    seconds  calls  us/call us/call name
67.17   168.81     168.81  50000  3376.20 5023.11 function2
32.83   251.32      82.51  50100  1646.91 1646.91 function1

    上面报表中显示了两个函数的统计信息:
  • time字段为函数执行时间占总执行时间的百分比,function2函数占了67.17%
  • cumulative seconds为所有函数的累积时间,从第一个函数一直累加下去,function2累积时间是168.81秒,function1加上function2的累积时间为251.32秒
  • self seconds字段为函数自身运行的总时间(不包括函数内子函数的运行时间),function2的自身运行时间为168.81秒,这个时间不包括function2内部调用的function1函数的时间
  • calls字段为调用次数,function2函数调用了50000次,function1函数首先在main函数内被调用了100次,而且在function2每次被调用时也会调用function1函数,function2被调用50000次,那么function1也会被调用50000次,50000加100得到function1一共被调用50100次
  • self us/call字段为函数每次调用的自身执行微秒数(不包括内部调用的子函数的执行时间),function2平均每次调用会消耗3376.20us
  • total us/call字段为函数每次调用的总执行微秒数,包括了内部调用的子函数的执行时间。function2的该字段为5023.11us ,5023.11 = 3376.20 + 1646.91 (即自身执行时间加上function1子函数的执行时间),function1的self us/call和total us/call都是1646.91us,因为它内部并没有调用其他的子函数。
    gprof.txt的第二个部分call graph的内容:

index % time   self  children    called     name
                                                
[1]   100.0    0.00  251.32                 main [1]
             168.81   82.35   50000/50000       function2 [2]
               0.16    0.00     100/50100       function1 [3]
-----------------------------------------------
             168.81   82.35   50000/50000       main [1]
[2]    99.9  168.81   82.35   50000         function2 [2]
              82.35    0.00   50000/50100       function1 [3]
-----------------------------------------------
               0.16    0.00     100/50100       main [1]
              82.35    0.00   50000/50100       function2 [2]
[3]    32.8   82.51    0.00   50100         function1 [3]
-----------------------------------------------

    上面显示了main , function2 , function1三个函数之间的调用关系,以及各自的执行时间等信息,例如第三条中function1执行时间占总执行时间的32.8%,第二条function2占99.9% (这个百分比包含了内部子函数调用的执行时间)。第三条中function1函数在main函数里100次循环调用所占时间为0.16秒,function1在function2函数中被调用50000次的总执行时间为82.35秒,所以function1的总执行时间为82.51 = 82.35 + 0.16 ,其他条目的信息可以进行类似的分析。

A Complete Assembly Development System (一个完整的汇编开发系统):

    现在你知道了汇编开发环境的所有所需的工具,使用GNU开发工具最好的平台之一莫过于Linux操作系统,下面就介绍下Linux系统。

The basics of Linux (Linux基础知识):

    如果你是linux的新手,那么你首先需要了解一些linux发行版的背景知识,linux的各种发行版,通常指的是包含了很多程序的一整套系统,这套完整的系统又可以分为两大块,一块是linux Torvalds及其他开发人员开发和维护的linux内核部分,内核就是系统的灵魂,它负责和底层打交道,所有上层的用户程序都是通过内核来实现文件访问,内存控制,及其他硬件控制等,内核作为一个虚拟平台,将计算机一分为二:上层的用户程序和底层的硬件,内核就负责初始化底层硬件,并为用户程序提供接口以访问所需的硬件设备。整套系统的第二大块就是用户程序,内核只提供了基础的接口,还需要丰富的用户程序来满足人们的多种多样的应用需求。

    各种GNU工具就是linux发行版的用户程序部分,在linux下可以使用包管理器来管理各种软件包,例如Red Hat下的rpm,Debian下的dpkg等。如果没有包管理器,那么你可能需要通过手动下载编译源代码的方式来安装软件,编译源代码时你还需要自己解决各种繁琐的依赖问题,当然使用包管理器也会遇到软件包依赖问题,不过用包管理器解决起来会更容易些。

    安装linux比较流行的方式就是用各种发行版的启动CD盘来安装。英文原著的作者推荐了一款linux发行版,即MEPIS linux系统,该系统是基于Debian的系统,作者之所以推荐这款发行版是因为该发行版的系统中默认就包含了gas , ld , gcc , gprof , gdb , kdbg等开发工具,只要从MEPIS的CD盘启动,你就拥有了一套完整的汇编开发环境了。

Downloading and running MEPIS (下载运行MEPIS):

    MEPIS的官网:www.mepis.org 下面是首页的一张介绍性的截图:


图3

    上图可以看出来,MEPIS系统快捷方便,使用KDE作为桌面环境,内核目前使用的是2.6.36的版本(官网觉得这个版本比较稳定)。

    可以访问顶层菜单"Get MEPIS"下的子菜单"Download MEPIS",从该子菜单对应的页面中下载ISO镜像,然后烧录成CD盘(可能有别的方法直接用grub4dos之类的进行硬盘安装,因为译者用的slackware,没用过该系统,所以不好多作说明),从CD盘启动,可以使用图形界面的向导来进行安装,网上说它的硬件检测能力很强,安装基本无需人工干预,就是汉化比较麻烦,不知道现在的版本情况怎么样。读者可以在网上找些资料来进行安装尝试,可以先使用ISO文件在VirtualBox等虚拟机上进行试装,觉得好再决定是否需要安装到真机中。

    你当然也可以选择别的发行版系统,例如译者使用的slackware装好后,也有完整的GNU开发环境等。萝卜白菜各有所好,这里限于篇幅就不多说了,一句话,找一个你喜欢的linux发行版进行安装,然后用包管理器或源代码编译等方式将前面提到过的GNU开发工具等安装好,一个汇编开发系统就有了。

    当你拥有了一套完整的开发环境后,就可以开始正式的学习汇编开发了。接下来的章节将通过案例来学习汇编语言。

    OK,到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 汇编开发示例 (一)

上一篇: 汇编开发相关工具 (二)

相关文章

什么是汇编语言(一) 汇编底层原理,指令字节码

汇编中使用文件 (二)

使用内联汇编 (一)

全书结束篇 使用IA-32平台提供的高级功能 (四) SSE2、SSE3相关指令

汇编函数的定义和使用 (三) 汇编函数结束篇

调用汇编模块里的函数 (一)