前一篇中提到的示例代码在汇编时,对于简单的语法错误,比如写错了某个助记符等可以根据汇编器的错误提示找到,但是如果是程序的逻辑错误,那么汇编器就力不从心了,这时就需要用到调试器了,下面就具体介绍GNU调试器在汇编开发中的用法...

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

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

Debugging the Program (调试汇编程序):

    前一篇中提到的示例代码在汇编时,对于简单的语法错误,比如写错了某个助记符等可以根据汇编器的错误提示找到,但是如果是程序的逻辑错误,那么汇编器就力不从心了,这时就需要用到调试器了,下面就具体介绍GNU调试器在汇编开发中的用法。

Using gdb (gdb调试器的使用):

    为了能够使用gdb调试器,你必须首先使用-gstabs参数来重新进行汇编:

$ as -gstabs -o cpuid.o cpuid.s
$ ld -o cpuid cpuid.o
$

    -gstabs参数会将额外的调试信息加入到程序中,这样gdb就可以根据这些调试信息来完成调试工作,额外的调试信息会增加程序的体积,而且生成的指令集也是没经过优化的,所以执行速度会受到些影响,所以请确保只是在调试情况下才使用-gstabs参数。

    可以通过实验来验证-gstabs生成的程序要比正常生成的程序要大,在没有-gstabs参数情况下生成的可执行文件如下:

-rwxr-xr-x 1 rich rich 771 2004-07-13 07:32 cpuid

    当使用-gstabs时,生成的可执行文件如下:

-rwxr-xr-x 1 rich rich 1099 2004-07-13 07:20 cpuid

    可以看出来,文件大小由771字节增加到了1099字节,在本例中大小相差不大,但是试想下,如果有个1万多行代码的汇编程序,那么两种情况下生成的文件大小就会相差很大了,所以不必要的情况下,最好不要生成调试信息。

Stepping through the program (一步步调试程序):

    现在可执行文件中包含了必要的调试信息,你就可以使用gdb来调试程序了:

$ gdb cpuid
GNU gdb 6.0-debian
Copyright 2003 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-linux”...

(gdb)

    gdb启动时,被调试程序已经被加载到内存中了,你可以在gdb提示符下使用run命令来运行程序:

(gdb) run
Starting program: /home/rich/palp/chap04/cpuid
The processor Vendor ID is ‘GenuineIntel’

Program exited normally.

(gdb)

    从输出结果可以看出来,程序运行结果和直接在命令行下运行的结果一致。

    现在我们需要一行行的调试源代码,要做到这点,就必须先在程序中设置一个断点,所谓断点就是指在某种条件下让程序暂停下来,然后就可以通过命令来一行行的执行代码,并查看程序里的一些寄存器,内存值等相关信息了,你可以设置下面几种类型的断点:
  • 通过代码中的标签来设置断点
  • 通过源代码中的行号来设置断点
  • 当某个数据达到指定的值时设置断点
  • 当某个函数被执行了指定次数后设置断点
    下面我们就通过标签来在代码的开头设置断点,让程序一执行就暂停下来,然后就可以调试出程序的完整的执行过程了,下面是设置标签断点的命令格式:

break *label+offset

    break是设置断点的gdb命令,后面跟的是下断点的位置,label是代码中的某个标签,offset是距离label标签的偏移量即断点距离label标签的行数(offset是可选的),按照常规思维,我们一开始会用下面的方法在示例程序的开头设置断点:

(gdb) break *_start
Breakpoint 1 at 0x8048075: file cpuid.s, line 11.
(gdb) run
Starting program: /home/rich/palp/chap04/cpuid
The processor Vendor ID is ‘GenuineIntel’

Program exited normally.

(gdb)

    因为我们的cpuid示例代码中只有一个_start的入口标签,所以我们用break *_start的命令,但是使用run命令运行程序后,程序却没有在一开始暂停下来,而是不顾断点一直执行下去了,译者注释:该BUG只在旧版本的gdb中存在,英文原著使用的是gdb 6.0的版本,该版本下就存在此BUG,在译者的slackware linux中,使用gdb 7.2的版本就没有这个BUG,设置_start断点后,就可以在开头断下来,所以如果你使用的是存在此BUG的旧版本的gdb,那么就可以使用下面的方法来处理此BUG,即在_start标签后添加一个NOP的伪指令(该指令什么也不做,在此处就占一行代码,可以在该位置设置断点,有的汇编程序中也会使用NOP来占用额外的时钟周期,以让前面的指令能充分执行完),如下所示:

_start:
    nop
    movl $0, %eax
    cpuid

    在添加了NOP指令后,重新用-gstabs来汇编和链接程序,接着在_start+1处设置断点:

(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file cpuid.s, line 12.
(gdb) run
Starting program: /home/rich/palp/chap04/cpuid

Breakpoint 1, _start () at cpuid.s:12
12 movl $0, %eax
Current language: auto; currently asm

(gdb)

    可以看到调试器在预定的位置暂停下来,现在就可以使用next或step命令来单步调试程序了:

(gdb) next
_start () at cpuid.s:13
13 cpuid
(gdb) next
_start () at cpuid.s:14
14 movl $output, %edi

(gdb) step
_start () at cpuid.s:15
15 movl %ebx, 28(%edi)

(gdb) step
_start () at cpuid.s:16
16 @code last w/screen:movl %edx, 32(%edi)

    next和step命令一次只执行一条汇编指令,每执行完一条指令就会显示下一次将要执行的指令的行号和代码信息,当你单步调试完你感兴趣的代码区段后,就可以使用cont命令来按常规的方式继续执行后面的代码,在使用cont命令后,程序会一路执行下去,直到遇到其他的断点或程序结束为止:

(gdb) cont
Continuing.
The processor Vendor ID is ‘GenuineIntel’

Program exited normally.

(gdb)

    之前输出的调试信息是英文原著作者使用的gdb 6.0版本的输出情况,不同的版本输出情况可能会有所差异。在设置断点和单步调试的情况下,你还可以查看某些数据的值,下面就介绍如何使用gdb来查看所需数据的值。

Viewing the data (查看数据):

    gdb提供了几种不同的命令来查看不同类型的数据,其中最常见的数据元素是寄存器和内存位置的值,下表显示了和查看数据元素相关的命令:

Data Command Description
info registers Display the values of all registers 
显示出所有寄存器里的值
print Display the value of a specific register 
or variable from the program 
显示出程序中指定寄存器或变量的值
x Display the contents of a specific 
memory location 
显示出指定内存位置的内容

    下面是info registers命令的输出情况:

(gdb) s
_start () at cpuid.s:13
13 cpuid
(gdb) info registers
eax		0x0 0
ecx		0x0 0
edx		0x0 0
ebx		0x0 0
esp		0xbffffd70 0xbffffd70
ebp		0x0 0x0
esi		0x0 0
edi		0x0 0
eip		0x804807a 0x804807a
eflags		0x346 838
cs		0x23 35
ss		0x2b 43
ds		0x2b 43
es		0x2b 43
fs		0x0 0
gs		0x0 0
(gdb) s
_start () at cpuid.s:14
14	movl $output, %edi
(gdb) info registers
eax		0x2 2
ecx		0x6c65746e 1818588270
edx		0x49656e69 1231384169
ebx		0x756e6547 1970169159
esp		0xbffffd70 0xbffffd70
ebp		0x0 0x0
esi		0x0 0
edi		0x0 0
eip		0x804807c 0x804807c
eflags		0x346 838
cs		0x23 35
ss		0x2b 43
ds		0x2b 43
es		0x2b 43
fs		0x0 0
gs		0x0 0
(gdb)

    在CPUID指令执行前,EAX , ECX , EDX , EBX这4个寄存器里的值都是0 ,在CPUID指令执行后,这4个寄存器里就存放了CPUID相关的输出数据(就是前面提到过的处理器的供应商ID字符串信息等)。

    print命令也可以显示指定寄存器的值,print命令可以配合修饰符来声明要输出数据的格式:
  • print/d 将值以十进制的形式显示出来
  • print/t 将值以二进制的形式显示出来
  • print/x 将值以十六进制的形式显示出来
    下面是print命令的例子:

(gdb) print/x $ebx
$9 = 0x756e6547
(gdb) print/x $edx
$10 = 0x49656e69
(gdb) print/x $ecx
$11 = 0x6c65746e
(gdb)

    最后是x命令,可以用于显示指定内存位置处的内容,和print命令类似,x命令也可以指定和输出相关的修饰符,x命令的格式如下:

x/nyz

    其中n用于指明要显示的字段数目,y用于指明输出的格式,有如下几种输出格式:
  • c代表字符输出格式
  • d代表十进制输出格式
  • x代表十六进制输出格式
    z修饰符用于指示输出中每个字段的大小,有如下几种字段大小:
  • b代表字节大小
  • h代表16位的word字大小(即half-word , word的一半大小)
  • w代表32位的word字大小
    下面是使用x命令来显示出output标签内存处的内容的例子:

(gdb) x/42cb &output
0x80490ac <output>:84 ‘T’ 104 ‘h’ 101 ‘e’ 32 ‘ ‘ 112 ‘p’ 114 ‘r’ 111 ‘o’99 ‘c’
0x80490b4 <output+8>:101 ‘e’ 115 ‘s’ 115 ‘s’ 111 ‘o’ 114 ‘r’ 32 ‘ ‘ 86 ‘V’ 101 ‘e’
0x80490bc <output+16>:110 ‘n’ 100 ‘d’ 111 ‘o’ 114 ‘r’ 32 ‘ ‘ 73 ‘I’ 68 ‘D’ 32 ‘ ‘
0x80490c4 <output+24>:105 ‘i’ 115 ‘s’ 32 ‘ ‘ 39 ‘\’’ 71 ‘G’ 101 ‘e’ 110 ‘n’117 ‘u’
0x80490cc <output+32>:105 ‘i’ 110 ‘n’ 101 ‘e’ 73 ‘I’ 110 ‘n’ 116 ‘t’ 101 ‘e’108 ‘l’
0x80490d4 <output+40>:39 ‘\’’ 10 ‘\n’

(gdb)

    上面的命令将output标签所在内存位置开始的42个字节的数据以字符的形式显示出来(同时也显示出了字符对应的十进制值),当需要追踪那些操作内存数据的指令时,此功能就很有用了。

Using C Library Functions in Assembly (在汇编程序中使用C的库函数):

    前面的汇编示例中是通过int $0x80这种linux系统调用的形式来显示字符串信息的,其实,还可以像C语言那样,直接使用标准的C库函数来打印信息,下面就详细的说明如何在汇编程序中使用C的库函数。

Using printf (在汇编程序中使用printf函数):

    在安装有GNU C编译器的系统上,可以很轻松的使用通用的C库函数,下面就使用printf函数来写第二个版本的cpuid程序,我们将汇编代码文件命名为cpuid2.s,代码如下:

#cpuid2.s View the CPUID Vendor ID string using C library calls
.section .data
output:
    .asciz "The processor Vendor ID is '%s'\n"
.section .bss
    .lcomm buffer, 12
.section .text
.globl _start
_start:
    movl $0, %eax
    cpuid
    movl $buffer, %edi
    movl %ebx, (%edi)
    movl %edx, 4(%edi)
    movl %ecx, 8(%edi)
    pushl $buffer
    pushl $output
    call printf
    addl $8, %esp
    pushl $0
    call exit

    printf函数可以根据显示需要接受可变的多个参数,第一个参数是输出的格式字符串,后面的参数都是根据格式字符串来确定的:

output:
    .asciz "The processor Vendor ID is '%s'\n"

    output对应的字符串信息就是格式字符串,%s是个字符串占用标识,表示printf函数的第二个参数将会填充替换掉%s,值得注意的是,此处使用的是.asciz ,而非.ascii ,这是因为printf函数需要接受以null字符结尾的字符串信息(null字符就是ASCII值为0的字符),.asciz伪操作符就会自动在定义的字符串末尾加上null字符。

    printf函数的第二个参数在本例中就是bss段中定义的buffer局部变量,buffer变量使用.lcomm伪操作符进行声明,说明这是一个局部变量,局部变量对于ld链接器是不可见的,同时还指定了buffer缓冲区域在bss段中的大小为12字节:

.section .bss
    .lcomm buffer, 12

    在CPUID指令执行完后,寄存器中包含的供应商ID字符串信息将被放置到buffer变量所在的内存区域。

    要将output和buffer两个参数传递给printf函数,就必须将它们压入栈中,通过PUSHL指令就可以完成压栈操作,不过需要特别注意的是,参数压栈的顺序是和函数声明中参数的顺序刚好相反的,也就是说第一个参数会被最后压入栈,而最后一个参数会被最先压入栈,这是和C函数查找参数的方式有关,一般C函数会从栈顶开始往下查找第一个参数,所以第一个参数必须最后一个压入栈,在本例中就是buffer参数会被最先压入栈,output参数则最后压入栈,在将参数都压入栈中后,就可以通过CALL指令来调用printf函数:

pushl $buffer
pushl $output
call printf
addl $8, %esp

    上面代码中,最后一条代码通过ADDL指令将ESP寄存器里的值加8,ESP是栈顶指针寄存器,一开始指向的是栈顶也是第一个参数的位置,每个参数占用4个字节,ESP加8后,就可以将栈中的两个参数给丢弃掉,因为如果以后还有压栈操作的话,那么新的值就会覆盖掉原来的参数,所以通过简单的加8,就可以将参数给清理掉。

    调用exit函数,也是同样的方法,先将参数0即程序的退出码压入栈,然后CALL来调用exit函数:

pushl $0
call exit

Linking with C library functions (链接C的库文件,这样才能在汇编程序中使用C库函数):

    当你的汇编程序中使用了C库函数时,就必须在链接生成可执行文件时,将C的库文件也链接进去,否则就会出现如下的链接错误:

$ as –o cpuid2.o cpuid2.s
$ ld -o cpuid2 cpuid2.o

cpuid2.o: In function `_start’:
cpuid2.o(.text+0x3f): undefined reference to `printf’
cpuid2.o(.text+0x46): undefined reference to `exit’

$

    上面的错误是说无法找到printf和exit函数的引用,通过链接C库文件就可以解决此问题,在linux系统中,有两种链接库文件的方法,一种是静态链接,静态链接会将库函数所在的目标代码给链接进可执行文件中,这种方式一方面会增加可执行文件的体积,另一方面程序执行时,会在内存中存在多个库函数的副本,导致内存浪费。

    第二种是动态链接,动态链接的方式只是将函数的位置信息写入到可执行文件中,在可执行文件运行时,对应的动态库文件才会被操作系统加载,并且加载的动态库文件在内存中是可以被多个程序访问的,不会像静态链接那样存在很多副本,这样既减少了可执行文件的体积,同时也防止了内存资源的浪费。

    在linux系统中,标准的C动态库的文件名是libc.so.x ,其中x表示动态库的版本号,例如在英文原著作者的MEPIS系统上,对应的C动态库文件名为libc.so.5 ,这个库文件中就包含了所需的标准C函数,如printf和exit函数。

    在使用gcc时,这个文件会被自动链接进C程序中,但是对于汇编程序,你就必须手动将库文件链接进去,GNU链接器提供了-l参数来指定需要链接的库文件,当然你不需要将库文件的完整路径都指出来,链接器会假设库文件名为以下格式:

/lib/libx.so

    上面的x就是-l参数中指定的名称,在本例中就是字符'c',可以看到这里用的是libx.so,而非libx.so.x,这是因为在linux系统中通常会为libx.so.x建立一个指向它的符号链接即libx.so(有点像windows中的快捷方式),所以直接用libx.so即可,完整的链接命令如下:

$ ld -o cpuid2 -lc cpuid2.o
$ ./cpuid2

bash: ./cpuid2: No such file or directory
$

    有意思的是,上面ld链接时并没有什么错误,但是在运行生成的可执行文件cpuid2时却出现了错误,出现这个错误的原因就在于,虽然通过-l参数将C的动态链接库信息链接进了程序中,但是在运行时,程序本身是无法加载任何动态库文件的,它需要借助第三方的动态库加载程序来完成加载工作,对应linux系统,可以使用ld-linux.so.2程序,这个程序通常位于/lib目录中,可以在GNU链接器中通过-dynamic-linker参数来指定第三方的加载程序:

$ ld -dynamic-linker /lib/ld-linux.so.2 -o cpuid2 -lc cpuid2.o
$ ./cpuid2

The processor Vendor ID is ‘GenuineIntel’
$

    现在程序就可以正常运行了,它通过ld-linux.so.2在运行时动态的加载libc.so库文件,从而让汇编程序中的C库函数得以正常的执行。

    如果你使用gcc来编译汇编程序的话,gcc就会自动将这些标准的C库文件和所需的第三方加载程序都链接进可执行文件中,不过使用gcc需要将_start标签改成main标签,这点在前面解释过了,使用gcc就只需一行简单的命令:

$ gcc -o cpuid2 cpuid2.s
$ ./cpuid2

The processor Vendor ID is ‘GenuineIntel’
$

    最后就是总结部分,同样因为篇幅略过,本篇和上一篇介绍了示例代码,但是并没详细讲解里面的指令和数据格式,只是初略的说明了一下,从下节开始就将详细的介绍汇编语法等各种基础知识,下一篇主要介绍数据操作方面的内容。

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

下一篇: Moving Data 汇编数据移动 (一)

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

相关文章

汇编流程控制 (二) 条件分支及汇编循环

Moving Data 汇编数据移动 (一)

高级数学运算 (二) 基础浮点运算

汇编函数的定义和使用 (一)

基本数学运算 (三) 除法和移位指令

汇编中使用文件 (三) 使用文件结束篇