通过GDB学习C语言
作者:网络转载 发布时间:[ 2015/8/25 11:21:20 ] 推荐标签:编程语言 .NET
GDB 带来了一个功能强大的工具,能够直接检测内存:x 命令。x 命令从一个特定的地址开始检测内存。结合一些结构化的命令和这些已给的命令能精确控制你想检测多少字节,你想怎样打印它们。当你有疑问时,尝试在 GDB 提示下运行 help x。
& 操作符计算变量的地址,这意味着我们能将 &i 返回给 x,从而看到 i 值背后原始的字节。
(gdb) x/4xb &i
0x7fff5fbff584: 0x39 0x05 0x00 0x00
标识参数表示我想要检查 4 个值,格式是十六进制,一次显示一个字节。我选择检查 4 个字节,是因为 i 在内存中的大小是 4 字节;逐字节打印出 i 在内存中的表示。
在 Intel 机器上有一个坑应当记得,逐字节检测时字节数是以“小端”顺序保存:不像人类一般使用的标记方法,一个数字的低位在内存中排在前面(个位数在十位数之前)。
为了让这个问题更加明显,我们可以为 i 赋一个特别的值,然后重新检测所占内存。
(gdb) set var i = 0x12345678
(gdb) x/4xb &i
0x7fff5fbff584: 0x78 0x56 0x34 0x12
使用 ptype 检查类型
ptype 命令可能是我喜爱的命令。它告诉你一个 C 语言表达式的类型。
(gdb) ptype i
type = int
(gdb) ptype &i
type = int *
(gdb) ptype main
type = int (void)
C 语言中的类型可以变得很复杂,但是好在 ptype 允许你交互式地查看他们。
指针和数组
数组在C语言中是非常难以捉摸的概念。这节的计划是写出一个简单的程序,然后在 GDB 中运行,直至它的意义变得清晰易懂。
编写如下的程序,array.c:
int main()
{
int a[] = {1,2,3};
return 0;
}
使用 -g 作为命令行参数进行编译,在 GDB 中运行,然后输入 next,执行初始化那一行
$ gcc -g arrays.c -o arrays
$ gdb arrays
(gdb) break main
(gdb) run
(gdb) next
在这里,你应该能够打印出 a 的内容并检查它的类型:
(gdb) print a
$1 = {1, 2, 3}
(gdb) ptype a
type = int [3]
现在我们的程序已经在 GDB 中运行起来了,我们应该做的第一件事是使用 x 看看 a 在内存中是什么样子。
(gdb) x/12xb &a
0x7fff5fbff56c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
0x7fff5fbff574: 0x03 0x00 0x00 0x00
以上意思是 a 所占内存开始于地址 0x7fff5fbff5dc。起始的四个字节存储 a[0], 随后的四个字节存储 a[1], 后的四个字节存储 a[2]。事实上你可以通过 sizeof 得到,a 在内存中的大小是 12 字节。
(gdb) print sizeof(a)
$2 = 12
现在,数组好像确实有个数组的样子。他们有自己的数组类型,在连续的内存空间中存储自己的成员。然而在某些情况下,数组表现得更像指针。例如,我们能在 a 上进行指针运算。
= preserve do
:escaped
(gdb) print a + 1
$3 = (int *) 0x7fff5fbff570
字面上看,a+1 是一个指向 int 的指针,占据地址 0x7fff5fbff570。这时,你应该反过来将指针传递给 x 命令,让我们看看会发生什么:
= preserve do
:escaped
(gdb) x/4xb a + 1
0x7fff5fbff570: 0x02 0x00 0x00 0x00
注意 0x7fff5fbff570 比 0x7fff5fbff56c 大 4,后者是 a 在内存地址中的第一个字节。考虑到 int 值占 4 字节,这意味着 a+1 指向 a[1].
事实上,在 C 语言中数组索引是指针运算的语法糖:a[i] 等于 *(a+i)。你可以在 GDB 中尝试一下。
= preserve do
:escaped
(gdb) print a[0]
$4 = 1
(gdb) print *(a + 0)
$5 = 1
(gdb) print a[1]
$6 = 2
(gdb) print *(a + 1)
$7 = 2
(gdb) print a[2]
$8 = 3
(gdb) print *(a + 2)
$9 = 3
我们已经看到在某些情况下,a 表现的像一个数组,在另一些情况下表现得像一个指向它首元素的指针。接下来会发生什么呢?
答案是当一个数组名在 C 语言表达式中使用时,它“退化”成指向这个数组首元素的指针。这个规则只有两个例外:当数组名传递给 sizeof 函数时,当数组名传递给操作数 & 时。
事实上,a 在传递给操作数 & 时并没有“退化”成一个指针,这带来一个有趣的问题:由“退化”变成的指针和 &a 存在区别吗?
数值上讲,他们都表示相同的地址:
= preserve do
:escaped
(gdb) x/4xb a
0x7fff5fbff56c: 0x01 0x00 0x00 0x00
(gdb) x/4xb &a
0x7fff5fbff56c: 0x01 0x00 0x00 0x00
然而,他们的类型是不同的。我们已经看到 a 退化的值是指向 a首元素的指针;这个必须是类型 int *。对于类型 &a,我们可以直接询问 GDB:
= preserve do
:escaped
(gdb) ptype &a
type = int (*)[3]
从显示上看,&a 是一个指向 3 个整数数组的指针。这说明:当传递给 & 时,a 没有退化,a 有了一个类型,是 int[3]。
通过测试他们在指针运算时的表现,你可以观察到 a 的退化值和 &a 的明显区别。
= preserve do
:escaped
(gdb) print a + 1
$10 = (int *) 0x7fff5fbff570
(gdb) print &a + 1
$11 = (int (*)[3]) 0x7fff5fbff578
注意到对 a 增加 1 等于对 a 的地址增加 4,与此同时,对 &a 增加 1 等于对 a 的地址增加 12!
实际上 a 退化成的指针是 &a[0];
= preserve do
:escaped
(gdb) print &a[0]
$11 = (int *) 0x7fff5fbff56c
结论
希望我已经向你证明 GDB 是学习 C 语言的一个灵巧而有富有探索性的环境。你能使用 print 打印表达式的值,使用 x 查看内存中原始字节,使用 ptype 配合类型系统进行问题修补。
如果你想要进一步对使用 GDB 学习 C 语言进行尝试,我有一些建议如下:
1.用 gdb 通过 Ksplice 指针挑战。
2.研究结构体是怎样在内存中存储的? 他们与数组比较又有什么异同?
3.使用 GDB 的 disassemble 命令学习汇编语言!一个特别有趣的练习是研究函数调用栈是如何工作的。
4.试试 GDB 的 “ tui ”模式,这个模式在常规 GDB 顶层提供一个图像化的 ncurses 层(Ncurses 提供字符终端处理库,包括面板和菜单)。在 OS X 系统中,你可能需要用源代码安装 GDB。
Alan 是 Hacker School 的推广者。他想要感谢 David Albert、Tom Ballinger、Nicholas Bergson-Shilcock 和 Amy Dyer 给予非常有帮助的反馈。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。

sales@spasvo.com