[置顶] 用gdb来学习c语言(linux环境下)

目录(?)[+]

原文地址: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语言中出了名的难理解的部分——指针和数据的区别。

gdb的使用介绍

先创建一个如下的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) break main
(gdb) run

这个程序在第3行,变量i被初始化的时候时候暂停了。尽管i还没有被初始化,我们仍然可以用print命令看到它的值。

(gdb) print i
$3 = 32767

在C中,没有被初始化的局部变量的值是被定义的,所以你的GDB可能会打印出不同的值来!

我们可以用next 命令来执行当前行:

(gdb) next
(gdb) print i
$4 = 1337

用x命令查看内存

变量在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) 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中运行、测试好让你开始理解数组。

Arrays.c的代码如下:

int main()
{
    int a[] = {1,2,3};
    return0;
}

编译时代上-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
这里是的说明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

注意到a1的结果是向后加4个字节,相对的&a1的结果是向后加12

一个退化指针实际上是指向&a[0]的:

 (gdb) print &a[0]
$11 = (int *) 0x7fff5fbff56c

总结

希望我能使你明白gdb是学习C的一个便捷式的工具。你可以打印出表达式的值,查看内存的原始字节,还可以使用ptype查看变量类型。.

如果你打算用gdb来学习C,我有以下几点建议:

1.     使用gdb来学习 Ksplicepointer challenge.

2.     探索结构体是怎么储存在内存中的,它和数组又有什么不同?

3.     gdbdisassemble来学习汇编语言编程!一个最有趣的练习是探索函数调用过程的栈的工作方式。

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个字符

你可能感兴趣的:(gdb,内存,数组,指针,调试)