通过GDB学习C语言的讲解
对于那些具有高级编程语言诸如:Ruby、Scheme、Haskell等背景的人来说,学习C语言是具有挑战性的。除了纠结于C 语言中像手动内存管理和指针等底层特性外,你必须在没有REPL(Read-Eval-PrintLoop)的条件下完成工作。一旦你已经习惯于在REPL环境下进行探索性的编程,必须进行“编写-编译-运行”这样循环实在有点令人生厌。
最近我发现其实可以用GDB来作为C语言的伪REPL。我一直尝试使用GDB作为学习C语言的工具,而不仅仅是用来调试C程序,事实上这非常有趣。
这篇文章我的目的就是向你展示GDB是一个非常好的学习C语言工具。下面我将会向你介绍一些我最喜欢的GDB命令,然后我会向你阐述怎样使用GDB来理解C语言中一个出了名的复杂问题:数组和指针的区别。
GDB简介
从创建一个简单的C程序开始,minimal.c:
intmain() { inti=1337; return0; }
注意这个程序并没有做任何事情,也没有一条输出指令。拥抱使用GDB学习C语言的美丽新世界吧!
使用-g参数进行编译,这样会生成一些有助于debug,gdb可以利用的信息,编译后用GDB运行起来:
$gcc-gminimal.c-ominimal $gdbminimal
你现在应该能看到明显的GDB提示行。我之前告诉你这是一个REPL,下面我们就来试试:
(gdb)print1+2 $1=3
多么神奇!print是GDB的内置命令,他能够打印出一个C语言命令的返回值。如果你不确定一个GDB命令是做什么,尝试在GDB提示下运行命令help。
然后是一个更有趣的例子:
(gbd)print(int)2147483648 $2=-2147483648
这里我先忽略为什么2147483648==-2147483648;我想要说明的是即使是算术运算在C语言中也是有很多坑的,GDB能够理解运行C语言中的算术运算。
现在让我们在主函数中设置一个断点然后运行程序:
(gdb)breakmain (gdb)run
现在程序在第3行处暂停,正好在i进行初始化之前。有趣的是,尽管i还没有被初始化,我们依然能够使用print命令看到它的值。
(gdb)printi $3=32767
在C语言中,一个未被初始化的局部变量的值是没有定义的,所以你用GDB打印出的值可能与这里的不一样。
我们可以用next命令来执行当前断点这一行:
(gdb)next (gdb)printi $4=1337
使用x命令检查内存
在C语言中变量用来标示一块连续的内存区间。一个变量的内存区间由两个数字决定:
这块内存第一个字节数的数值地址
内存的大小,单位是字节。变量所占内容的大小取决于变量的类型。
C语言中一个独特的特性是你能够直接访问变量所占的内存。操作符&可以计算一个变量的地址,操作符sizeof计算变量所占内存的大小。
你可以在GDB中测试以上两个概念:
(gdb)print&i $5=(int*)0x7fff5fbff584 (gdb)printsizeof(i) $6=4
字面上看,i所占内存起始于地址0x7fff5fbff5b4,占内存4个字节。
我前面提到的变量在内存中的大小取决于它的类型,所以操作符sizeof能够直接作用于类型:
(gdb)printsizeof(int) $7=4 (gdb)printsizeof(double) $8=8
以上显示意味着,至少在我的计算机上int变量占4个字节空间,double变量占8个字节。
GDB带来了一个功能强大的工具,能够直接检测内存:x命令。x命令从一个特定的地址开始检测内存。结合一些结构化的命令和这些已给的命令能精确控制你想检测多少字节,你想怎样打印它们。当你有疑问时,尝试在GDB提示下运行helpx。
&操作符计算变量的地址,这意味着我们能将&i返回给x,从而看到i值背后原始的字节。
(gdb)x/4xb&i 0x7fff5fbff584:0x390x050x000x00
标识参数表示我想要检查4个值,格式是十六进制,一次显示一个字节。我选择检查4个字节,是因为i在内存中的大小是4字节;逐字节打印出i在内存中的表示。
在Intel机器上有一个坑应当记得,逐字节检测时字节数是以“小端”顺序保存:不像人类一般使用的标记方法,一个数字的低位在内存中排在前面(个位数在十位数之前)。
为了让这个问题更加明显,我们可以为i赋一个特别的值,然后重新检测所占内存。
(gdb)setvari=0x12345678 (gdb)x/4xb&i 0x7fff5fbff584:0x780x560x340x12
使用ptype检查类型
ptype命令可能是我最喜爱的命令。它告诉你一个C语言表达式的类型。
(gdb)ptypei type=int (gdb)ptype&i type=int* (gdb)ptypemain type=int(void)
C语言中的类型可以变得很复杂,但是好在ptype允许你交互式地查看他们。
指针和数组
数组在C语言中是非常难以捉摸的概念。这节的计划是写出一个简单的程序,然后在GDB中运行,直至它的意义变得清晰易懂。
编写如下的程序,array.c:
intmain() { inta[]={1,2,3}; return0; }
使用-g作为命令行参数进行编译,在GDB中运行,然后输入next,执行初始化那一行
$gcc-garrays.c-oarrays $gdbarrays (gdb)breakmain (gdb)run (gdb)next
在这里,你应该能够打印出a的内容并检查它的类型:
(gdb)printa $1={1,2,3} (gdb)ptypea type=int[3]
现在我们的程序已经在GDB中运行起来了,我们应该做的第一件事是使用x看看a在内存中是什么样子。
(gdb)x/12xb&a 0x7fff5fbff56c:0x010x000x000x000x020x000x000x00 0x7fff5fbff574:0x030x000x000x00
以上意思是a所占内存开始于地址0x7fff5fbff5dc。起始的四个字节存储a[0],随后的四个字节存储a[1],最后的四个字节存储a[2]。事实上你可以通过sizeof得到,a在内存中的大小是12字节。
(gdb)printsizeof(a) $2=12
现在,数组好像确实有个数组的样子。他们有自己的数组类型,在连续的内存空间中存储自己的成员。然而在某些情况下,数组表现得更像指针。例如,我们能在a上进行指针运算。
=preservedo :escaped (gdb)printa+1 $3=(int*)0x7fff5fbff570
字面上看,a+1是一个指向int的指针,占据地址0x7fff5fbff570。这时,你应该反过来将指针传递给x命令,让我们看看会发生什么:
=preservedo :escaped (gdb)x/4xba+1 0x7fff5fbff570:0x020x000x000x00
注意0x7fff5fbff570比0x7fff5fbff56c大4,后者是a在内存地址中的第一个字节。考虑到int值占4字节,这意味着a+1指向a[1].
事实上,在C语言中数组索引是指针运算的语法糖:a[i]等于*(a+i)。你可以在GDB中尝试一下。
=preservedo :escaped (gdb)printa[0] $4=1 (gdb)print*(a+0) $5=1 (gdb)printa[1] $6=2 (gdb)print*(a+1) $7=2 (gdb)printa[2] $8=3 (gdb)print*(a+2) $9=3
我们已经看到在某些情况下,a表现的像一个数组,在另一些情况下表现得像一个指向它首元素的指针。接下来会发生什么呢?
答案是当一个数组名在C语言表达式中使用时,它“退化”成指向这个数组首元素的指针。这个规则只有两个例外:当数组名传递给sizeof时,当数组名传递给操作数&时。
事实上,a在传递给操作数&时并没有“退化”成一个指针,这就带来一个有趣的问题:由“退化”变成的指针和&a存在区别吗?
数值上讲,他们都表示相同的地址:
=preservedo :escaped (gdb)x/4xba 0x7fff5fbff56c:0x010x000x000x00 (gdb)x/4xb&a 0x7fff5fbff56c:0x010x000x000x00
然而,他们的类型是不同的。我们已经看到a退化的值是指向a首元素的指针;这个必须是类型int*。对于类型&a,我们可以直接询问GDB:
=preservedo :escaped (gdb)ptype&a type=int(*)[3]
从显示上看,&a是一个指向3个整数数组的指针。这就说明:当传递给&时,a没有退化,a有了一个类型,是int[3]。
通过测试他们在指针运算时的表现,你可以观察到a的退化值和&a的明显区别。
=preservedo :escaped (gdb)printa+1 $10=(int*)0x7fff5fbff570 (gdb)print&a+1 $11=(int(*)[3])0x7fff5fbff578
注意到对a增加1等于对a的地址增加4,与此同时,对&a增加1等于对a的地址增加12!
实际上a退化成的指针是&a[0];
=preservedo :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提供字符终端处理库,包括面板和菜单)。在OSX系统中,你可能需要用源代码安装GDB。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。如果你想了解更多相关内容请查看下面相关链接