目录(?)[+]
原文地址:https://www.hackerschool.com/blog/5-learing-c-with-gdb
前几天在hacknews上看到这篇文章,发现它对C的初学者来说很有帮助。所以就尝试的翻译,粘贴在这里。限本人的英文水平和技术水平有限,有些地方可能翻译的不准,敬请见谅啊!
-----------------------------------------------------------------------------------
拥有Ruby,Scheme或者Haskell等高级语言背景的程序员学习c语言可能会非常有挑战性。特别是学习内存管理和指针等C的低级语言特征时,你没有REPL(read–eval–print loop)环境。一旦你习惯了用REPL来探索你的程序,那么write-compile-runloop编程的编程模式则可能会令你非常失望。
最近我发现我可以用gdb(一个C的伪REPL工具)。现在我一直在尝试使用gdb作为一种工具来学习C的,而不是仅仅调试C,而且这样非常有趣。
我的写这篇文章的目标是给你证明gdb是一个非常伟大的学习C的工具。我会介绍一些我最喜欢的GBD的指令,后面我会说明你怎么利用gdb来理解C语言中出了名的难理解的部分——指针和数据的区别。
先创建一个如下的C程序,minimal.c
int main()
{
int
i =
1337;
return0
;
}
这个程序不做任何事情并且没有一个printf语句。现在我们要鼓足勇气来通过gdb学习C语言了!
编译这个程序时加上-g参数,这样gdb就可以调试信息了。然后执行下面命令。
$ gcc -g minimal.c -o minimal
$ gdb minimal
你会发现你已经看到了gdb的prompt。我许诺过给你一个REPL,这里就是
(gdb) print 1 + 2
$1 = 3
真令人惊讶!GDB建立了一个print,并且打印出了一个C的表达式。如果你不能确定一个GDB指令到底的功能,可以试着在GDB prompt下执行help name-of-the-command。
这里有一些更有趣的示例:
(gbd) print (
int)
2147483648
$
2= -
2147483648
我们先忽略为什么2147483648 == -2147483648
。这里的重点是甚至一个计算在C中都可能会非常棘手,而GDB了解C的计算。
现在我们来在main函数中设置一个断点,然后开始运行程序:
(gdb)
breakmain
(gdb) run
这个程序在第3行,变量i被初始化的时候时候暂停了。尽管i还没有被初始化,我们仍然可以用print命令看到它的值。
(gdb) print i
$3 = 32767
在C中,没有被初始化的局部变量的值是被定义的,所以你的GDB可能会打印出不同的值来!
我们可以用next 命令来执行当前行:
(gdb) next
(gdb) print i
$4 = 1337
变量在c内存标签连续块。一个变量的内存块是由两个数字来决定的——块的第一个byte的地址和块的大小(以bytes为单位)。变量的大小是由变量的类型决定的。
一个C的特性是你需要直接访问变量的内存块。&运算符可以计算出一个变量的地址,而sizeof运算符可一个计算出变量占用的内存大小。
你可以在GDB中玩转这两个概念:
(gdb) print &i
$
5= (
int*)
0x7fff5fbff584
(gdb) print
sizeof(i)
$
6=
4
总而言之,i的内存块开始于0x7fff5fbff5b4,占用4个字节。我上面提到过一个变量的大小是由变量类型决定的。其实sizeof 操作可以直接作用在类型上:
(gdb) print
sizeof(
int)
$
7=
4
(gdb) print
sizeof(
double)
$
8=
8
这意味着,至少在你的机器上,int变量占用4个字节的空间,double占用8个。
使用GDB中的x命令,将会使它变成一个直接测试内存的强大的工具。X命令测量内存开始与一个特定的地址。它配备了一些格式化命令,提供精确的控制来实现你想检查多少字节,以及如何你想将它们打印出来。如果有疑问,在GDB prompt中执行help x。
&操作可以得到一个变量的地址,所以这意味着我们可以用x来执行&i并且来观察i值的原始字节。
(gdb) x/4xb &i
0x7fff5fbff584: 0x39 0x05 0x00 0x00
这个标志表示我想以4 个16进制数,一次一个字节的格式来显示。我已经选择查看4个字节,因为i的内存大小就是4个字节。输出信息中逐字节显示i的原始地址。
一个需要紧记于心的细节是如果在intel机器上的逐字节测试,其字节是按“little-endian”排序的。
一种更能清楚认识到这个特性的方法是赋予i一个更有趣的值,然后重新测试它的内存块:
(gdb)
setvar i =
0x12345678
(gdb) x/
4xb &i
0x7fff5fbff584:
0x780x56
0x34
0x12
Ptype也许是我最喜欢的命令了。它告诉你一个C表达式的类型:
(gdb) ptype i
type =
int
(gdb) ptype &i
type =
int*
(gdb) ptype main
type =
int(
void)
类型在C中可以会非常复杂,但是ptype可以让你时时的查看它们。
数组是C中一个很难以捉摸的概念。这个章节的目的就是写一个简单的程序,然后在GDB中运行、测试好让你开始理解数组。
Arrays.c的代码如下:
int main()
{
int
a[] = {
1,
2,
3};
return0
;
}
编译时代上-g参数,然后在GDB下运行它,执行next命令来完成初始化:
$ gcc -g arrays.c -o arrays
$ gdb arrays
(gdb)
breakmain
(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
这里是的说明x的内存块是从地址 0x7fff5fbff5dc开始的。a[0]储存在开始的四个字节中,下一个元素a[1]储存在紧随其后的四个字节中。最后四个字节储存a[2]。其实你可以用sizeof来查看a的内存大小,它占用了12个字节。
(gdb) print
sizeof(a)
$
2=
12
这这里,数组似乎相当数组。他们有自己的数组类型,在内存以中连续储存他们的元素。然而,在有些情况下,数组表现的和指针非常相似!举个例子,我们可以对a进行指针运算:
(gdb) print a +
1
$
3= (
int*)
0x7fff5fbff570
a+1的结果是一个指向int型的指针并且其内存地址是0x7fff5fbff570
是。此时,你应该条件反射地用x命令来查看此指针:
(gdb) x/4xb a + 1
0x7fff5fbff570: 0x02 0x00 0x00 0x00
注意, 0x7fff5fbff570比 a的首字节地址0x7fff5fbff56c大4。考虑到int占用4个字节,这意味着a+1是一个指向int型的指针。
实际上在C语言中,数组索引是指针运算的语法糖。a[i]和 *(a+i)等价。你可以在gdb中试试:
(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
我们看到在某些情况下它像一个数组,有些情况它又像一个指向第一个元素的指针。到底发生了什么?
答案就是当一个数组的名字被用在C的表达式中,它退化成了一个指向头元素的指针。但两种情况除外:当一个数组名被传递给了sizeof和当一个数组名被用在&操作下。
这是一个很有趣的问题,当一个数组名传递给&操作的时候,它没有退化成一个指针:指针和退化的指针和&a有什么不同吗?
从数值上看,他们都代表同一个地址:
(gdb) x/4xb a
0x7fff5fbff56c: 0x01 0x00 0x00 0x00
(gdb) x/4xb &a
0x7fff5fbff56c: 0x01 0x00 0x00 0x00
然而他们的类型是不同的。我们已经看到一个a的退化指针是指向它的头元素,所以个它的类型肯定是int*。而对于&a,我们可以直接问gdb:
(gdb) ptype &a
type =
int(*)[
3]
&a是一个指向拥有3个整型元素的数组指针。这里意味着:a在&操作下并没有退化,a的类型是int[3]。
你可以个通过指针运算来观察a的退化指针和&a的区别:
(gdb) print a +
1
$
10= (
int*)
0x7fff5fbff570
(gdb) print &a +
1
$
11= (
int(*)[
3])
0x7fff5fbff578
注意到a加1的结果是向后加4个字节,相对的&a加1的结果是向后加12!
一个退化指针实际上是指向&a[0]的:
(gdb) print &a[
0]
$
11= (
int*)
0x7fff5fbff56c
希望我能使你明白gdb是学习C的一个便捷式的工具。你可以打印出表达式的值,查看内存的原始字节,还可以使用ptype查看变量类型。.
如果你打算用gdb来学习C,我有以下几点建议:
1. 使用gdb来学习 Ksplicepointer challenge.
2. 探索结构体是怎么储存在内存中的,它和数组又有什么不同?
3. 用gdb的disassemble来学习汇编语言编程!一个最有趣的练习是探索函数调用过程的栈的工作方式。
4. 看看gdb的“tui”模式,它提供一个在常规gdb上的ncurses视图层。在OS X,你可能要从gdb的源码安装。
补充(gdb窗口模式)
进入gdb窗口模式:list命令,或者layout src命令,或者tui命令
其他代码窗口相关命令:
info win 显示窗口的大小
layout next 切换到下一个布局模式
layout prev 切换到上一个布局模式
layout src 只显示源代码
layout asm 只显示汇编代码
layout split 显示源代码和汇编代码
layout regs 增加寄存器内容显示
focus cmd/src/asm/regs/next/prev 切换当前窗口
refresh 刷新所有窗口
tui reg next 显示下一组寄存器
tui reg system 显示系统寄存器
update 更新源代码窗口和当前执行点
winheight name +/- line 调整name窗口的高度
tabset nchar 设置tab为nchar个字符
info win 显示窗口的大小
layout next 切换到下一个布局模式
layout prev 切换到上一个布局模式
layout src 只显示源代码
layout asm 只显示汇编代码
layout split 显示源代码和汇编代码
layout regs 增加寄存器内容显示
focus cmd/src/asm/regs/next/prev 切换当前窗口
refresh 刷新所有窗口
tui reg next 显示下一组寄存器
tui reg system 显示系统寄存器
update 更新源代码窗口和当前执行点
winheight name +/- line 调整name窗口的高度
tabset nchar 设置tab为nchar个字符