购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.5 使用调试器检查内存

现在我们已经开始编写程序了,你需要学习如何使用GNU调试器gdb。调试器是一种程序,允许你在运行另一个程序的同时观察和控制其行为。在使用调试器时,你有点像是一位木偶师,所调试的程序就是被精心控制的木偶。你的主要控制手段是断点;当程序运行到事先设置好的断点时,程序会暂停并将控制权返回给调试器。这时可以查看程序变量的值,这有助于你找出错误所在。

如果你觉得现在就谈论调试器似乎为时过早——我们的程序到目前为止都很简单,看起来用不着调试——我保证,对于学习使用调试器这件事,简单的例子绝对比复杂的例子效果好得多。

gdb也是学习本书的一个有价值的工具,即便你写的程序没有bug。例如,在后续的gdb会话中,我将向你展示如何确定变量在内存中的存储位置,以及如何以十进制和十六进制查看变量内容。你还会看到如何对正在运行的程序使用gdb,以此来演示先前讨论的概念。

下列gdb命令应该足够你上手了。更详细的内容参见第10章。

● b source_filename : line_number :在源代码文件 source_filename 的第 line_number 行设置断点。代码会在断点(即 line_number )处停止运行,将控制权返回给gdb,以便你检查代码的方方面面。

● c:从当前位置继续运行程序。

● h command :显示 command 的帮助信息。

● i r:显示CPU寄存器的内容(CPU寄存器的内容参见第9章)。

● l line_number :显示以指定的 line_number 为中心的10行源代码。

● print expression :显示 expression 的求值结果。

● printf " format ", var1 , var2 , ...:按照指定格式显示 var1 var2 等的值。 format 字符串遵循与C标准库中的printf相同的规则。

● r:开始运行受gdb控制的程序。

● x/ nfs memory_address :以格式 f 显示(检查)从 memory_address 开始,大小为 s n 个值。

2.5.1 使用调试器

让我们使用gdb过一遍清单2-1中的程序,探究一些到目前为止所涉及的概念。我建议你坐在计算机前边练边读:动手操作的时候,gdb会容易理解得多。注意,你的计算机上显示的地址可能与本例不同。

先使用gcc命令编译程序:

$ gcc -g -Wall -masm=intel -o intAndString intAndString.c

-g选项告诉编译器在可执行文件中加入调试器信息。-Wall选项告诉编译器对语法正确但可能与你的意图不符的代码发出警告。例如,编译器会警告你函数中存在没有用到的变量,这可能意味着你有遗漏。

当我们在本书后续部分编写汇编语言时,将用到Intel和AMD文档中指定的语法,我们会通过-masm=intel选项告诉编译器采用相同的语法。你现在还不需要指定它,但我建议你要习惯于这个选项,因为随后会用到。

-o选项指定了输出文件的名称,也就是可执行程序的名称。

程序编译好之后,我们就可以使用以下命令,在gdb的控制下运行该程序了:

$ gdb ./intAndString
--snip--
Reading symbols from ./intAndString…
(gdb)  l
1        /* intAndString.c
2         * Using printf to display an integer and a text string.
3         */
4
5       #include <stdio.h>
6
7       int main(void)
8       {
9         unsigned int anInt;
10        char aString[10];
(gdb)
11
12        printf("Enter a number in hexadecimal: ");
13        scanf("%x", &anInt);
14        printf("Enter it again: ");
15        scanf("%s", aString);
16
17        printf("The integer is %u and the string is %s\n", anInt, aString);
18
19        return 0;
20      }
(gdb)

为了节省篇幅,我删除了以上输出中的gdb启动消息,这些消息包含了调试器的相关信息以及使用方法参考文档。

l命令显示10行源代码,然后将控制权返回给gdb((gdb)提示符处)。按ENTER键重复前面的命令,l显示接下来的(最多)10行。

断点用于暂停程序,将控制权返回给调试器。我喜欢将断点设置在一个函数调用另一个函数的地方,以便在将参数传给被调用函数之前先检查参数的值。main函数在第17行调用printf,所以在此处设置断点。因为要设置的断点就在当前文件中,所以就不需要指定文件名了:

(gdb) b 17
Breakpoint 1 at 0x11f6: file intAndString.c, line 17.

如果gdb在运行程序的时候遇到了这句,就会在执行该语句之前暂停,将控制权返回给调试器。

设置好断点,运行程序:

  (gdb) r
  Starting program: /home/bob/progs/chapter_02/intAndString/intAndString
  Enter a hexadecimal value: 123abc
  Enter it again: 123abc
  
   Breakpoint 1, main () at intAndString.c:17
➊ 17         printf("The integer is %u and the string is %s\n", anInt, aString);

r命令从头开始运行程序。当程序抵达断点时,控制权返回gdb,后者显示出已经准备好执行的下一个语句➊。在继续执行之前,我要查看传给printf函数的两个变量:

(gdb) print anInt
$1 = 1194684
(gdb) print aString
$2 = "123abc\000\177\000"

我们可以使用print命令显示变量的当前值。gdb根据源代码获知每个变量的类型。对于int类型变量,以十进制形式显示;对于char类型变量,gdb会尽量显示与该码点值对应的字符字形。如果没有对应的字符字形,则显示码点(\之后跟3个八进制数码,参见表2-2)。例如,因为NUL没有字符字形,所以gdb在字符串末尾显示\000。

printf命令可以对显示的值进行格式化。格式字符串的用法与C标准库中的printf函数一样:

(gdb) printf "anInt = %u = %#x\n", anInt, anInt
anInt = 1194684 = 0x123abc
(gdb) printf "aString = 0x%s\n", aString
aString = 0x123abc

gdb提供了另一个可以直接检查内存内容(也就是实际的位模式)的命令x。该命令的帮助信息简明扼要,但你需要知道的一切都尽在其中:

 (gdb) h x
Examine memory: x/FMT ADDRESS.
ADDRESS is an expression for the memory address to examine.
FMT is a repeat count followed by a format letter and a size letter.
Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
 t(binary), f(float), a(address), i(instruction), c(char) and s(string).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).
The specified number of objects of the specified size are printed
according to the format.
Defaults for format and size letters are those previously used.
Default count is 1. Default address is following last thing printed
with this command or "print".

x命令需要待显示的内存区域的地址。我们可以使用print命令查找变量的地址:

(gdb) print &anInt
$3 = (unsigned int *) 0x7fffffffde88

接着使用x命令以3种不同的方式显示anInt的内容:以十进制形式显示一个字(1dw),以十六进制形式显示一个字(1xw),以十六进制形式显示4字节(4xb)。

(gdb) x/1dw 0x7fffffffde88
0x7fffffffde88: 1194684
(gdb) x/1xw 0x7fffffffde88
0x7fffffffde88: 0x00123abc
(gdb) x/4xb 0x7fffffffde88
0x7fffffffde88: ➊0xbc   0x3a    0x12    0x00
注意

字(word)的大小依赖于所使用的计算机环境。在我们的环境中,一个字是4字节。

4字节的显示对你来说可能看起来有点乱。第一字节➊位于行左侧显示的地址处。行中的下一字节位于后续地址0x7fffffffde89。因此,该行显示的4字节的内存地址分别为0x7fffffffde88、0x7fffffffde89、0x7fffffffde8a、0x7fffffffde8b,从左到右读取,组成了变量anInt的值。当分别显示这4字节时,最低有效字节在内存中先出现。这称为小端序(little-endian);我将在本次gdb之旅后进一步解释。

与此类似,我们先得到aString变量的地址,再显示其值:

(gdb) print &aString
$4 = (char (*)[50]) 0x7fffffffde8e

接着,我们以两种不同的方式显示aString的内容:10个字符(10c)和10个十六进制字节(10xb):

(gdb) x/10c 0x7fffffffde8e
0x7fffffffde8e: 49 '1'   50 '2' 51 '3' 97 'a' 98 'b' 99 'c' 0 '\000'
127 '\177'
0x7fffffffde96: 0 '\000' 0 '\000'
(gdb) x/10xb 0x7fffffffde8e
0x7fffffffde8e: 0x31     0x32   0x33    0x61    0x62    0x63    0x00
0x7f
0x7fffffffde96: 0x00     0x00

字符形式显示了每个字符十进制的码点及其对应的字形。十六进制字节形式只显示了每个字节的码点(十六进制)。在这两种显示形式中,NUL字符都标记了6字符长的字符串的结尾。因为我们要求显示10字节,所以剩下的3字节都是与字符串无关的随机值,通常称为垃圾值。

最后,继续执行程序,然后退出gdb:

(gdb)c
Continuing.
The integer is 1194684 and the string is 123abc
[Inferior 1 (process 3165) exited normally]
(gdb)q
$

2.5.2 理解内存字节存储序

使用4字节和单字节显示内存地址0x7fffffffde88处的整数值,两者之间的差异演示了称为字节序或字节存储顺序的概念。我们通常从左到右阅读数字。左边的数码比右边的数码更重要(代表更多的计数)。

小端(little-endian)

对于存储在内存中的数据,多字节值中的最低有效字节位于最低编号的地址。也就是说,“最小”(littlest)的字节(计数最少)位于内存的最前面。

当我们逐字节检查内存时,每个字节按照地址递增顺序出现:

0x7fffffffde88: 0xbc
0x7fffffffde89: 0x3a
0x7fffffffde8a: 0x12
0x7fffffffde8b: 0x00

猛一看,值的顺序似乎存储反了,因为最低有效字节(“小端”)最先存储在内存中。当我们命令gdb显示整个4字节值时,gdb知道当前是小端序环境,会以正确的顺序重新安排显示字节:

7fffffffde88: 000123abc

大端(big-endian)

对于存储在内存中的数据,多字节值中的最高有效字节位于最低编号的地址。也就是说,“最大”(biggest)的字节(计数最多)位于内存的最前面。

在大端存储中,最高有效(“最大”)字节出现在内存区域的第一个(编号最低的)地址处。如果我们在采用了大端存储(比如PowerPC架构)的计算机上运行先前的程序,会看到以下输出(假设变量位于相同的地址):

(gdb) x/1xw 0x7fffffffde88
0x7fffffffde88: 0x00123abc
(gdb) x/4xb 0x7fffffffde88          [BIG-ENDIAN COMPUTER, NOT OURS!]
0x7fffffffde88: 0x00    0x12    0x3a    0xbc

也就是说,大端计算机中的4字节是按照以下形式存储的:

0x7fffffffde88: 0x00
0x7fffffffde89: 0x12
0x7fffffffde8a: 0x3a
0x7fffffffde8b: 0xbc

同样,gdb知道这是一台采用了大端存储的计算机,因此会按照正确顺序显示这4字节。

在大多数编程情况下,字节序不是问题。但是,这不代表你不需要了解它,因为在调试器中检查内存时,字节序可能会令人困惑。当不同的计算机相互通信时,字节序也是一件麻烦事。例如,传输控制协议/互联网协议(TCP/IP)使用大端序,有时也称为网络字节序。x86-64架构使用的是小端序。操作系统会为网络通信调整字节序。但是如果你正在为操作系统或者可能没有操作系统的嵌入式系统编写通信软件,则必须要理解字节序。

动手实践

输入清单2-1中的程序。通过gdb跟踪程序。使用获取的数字,说明变量anInt和aString在内存中的存储位置以及每个位置存储的内容。 FxdrXfU4fPT4plQNrpnS8mUn7OtB7K+KO3ahdmVaHghr1Ub1CupgkSVj0Bd9Bzd3

点击中间区域
呼出菜单
上一章
目录
下一章
×