C语言学习:通过 GDB如何学习C语言

对于那些具有高级编程语言诸如: Ruby、Scheme、Haskell 等背景的人来说,学习 C 语言是具有挑战性的。除了纠结于 C  语言中像手动内存管理和指针等底层特性外,你必须在没有 REPL ( Read-Eval-Print Loop ) 的条件下完成工作。一旦你已经习惯于在 REPL 环境下进行探索性的编程,必须进行“编写-编译-运行”这样循环实在有点令人生厌。

最近我发现其实可以用 GDB 来作为 C 语言的伪 REPL。我一直尝试使用 GDB 作为学习 C 语言的工具,而不仅仅是用来调试 C 程序,事实上这非常有趣。

这篇文章我的目的就是向你展示 GDB 是一个非常好的学习 C 语言工具。下面我将会向你介绍一些我最喜欢的 GDB 命令,然后我会向你阐述怎样使用 GDB 来理解 C 语言中一个出了名的复杂问题:数组和指针的区别。

GDB 简介

从创建一个简单的 C 程序开始,minimal.c:

int main()

{

int i = 1337;

return 0;

}

注意这个程序并没有做任何事情,也没有一条输出指令。拥抱使用 GDB 学习 C 语言的美丽新世界吧!

使用 -g 参数进行编译,这样会生成一些有助于 debug,gdb 可以利用的信息,编译后用 GDB 运行起来:

$ gcc -g minimal.c -o minimal

$ gdb minimal

你现在应该能看到明显的 GDB 提示行。我之前告诉你这是一个 REPL,下面我们就来试试:

(gdb) print 1 + 2

$1 = 3

多么神奇! print 是 GDB 的内置命令,他能够打印出一个 C 语言命令的返回值。如果你不确定一个 GDB 命令是做什么,尝试在 GDB 提示下运行命令 help。

然后是一个更有趣的例子:

(gbd) print (int) 2147483648

$2 = -2147483648

这里我先忽略为什么 2147483648 == -2147483648;我想要说明的是即使是算术运算在 C 语言中也是有很多坑的,GDB 能够理解运行 C 语言中的算术运算。

现在让我们在主函数中设置一个断点然后运行程序:

(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 语言中变量用来标示一块连续的内存区间。一个变量的内存区间由两个数字决定:

这块内存第一个字节数的数值地址

内存的大小,单位是字节。变量所占内容的大小取决于变量的类型。

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 提示下运行 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。

原文地址:https://www.cnblogs.com/2f3d/p/10202285.html

时间: 2024-08-03 19:09:17

C语言学习:通过 GDB如何学习C语言的相关文章

《软件调试的艺术》学习笔记——GDB使用技巧摘要

<软件调试的艺术>学习笔记——GDB使用技巧摘要 <软件调试的艺术>,因为名是The Art of Debugging with GDB, DDD, and Eclipse. 作者是美国的Norman Matloff和Peter Jay Salzman,中文版由张云翻译.是人邮出版社图灵程序设计丛书初版.这里称为"艺术",个人觉得有点过了,但是其中关于gdb以及在gdb基础之上集成的DDD和Eclipse调试技巧的整理确实是做的很好,对于Linux/开源社区下的

递归学习(一)最简单的C语言递归求年龄算法

递归是我们在学习编程中,必须要去学习的,虽然递归晦涩难懂 ,但是很多时候,递归的思想会很有用,但是在实际开发中,不建议使用递归,要用循环来代替递归,不然bug无穷. ----------------------------------------------------------- 问题描述: 有5个人坐在一起, 问第5个人,他说比第4个人大2岁, 问第4个人,他说比第3个人大2岁, 问第3个人,他说比第2个人大2岁, 问第2个人,他说比第1个人大2岁, 问最后一个人,他说10岁 第5个人多大

[Go语言]从Docker源码学习Go——init()方法和identifier首字母大小写区分

init()方法 如果想在一个go文件里,进行一些初始化的工作,可以把代码放到init()方法中. init()方法先被执行. func init() { // initialization of package } 注意:它是没有输入参数和返回参数的. identifier首字母大小写区分 当identifier(比如constant,variable,type,function,struct field,...)的命名首字母是大写的话,那表示这个identifier在包外是可以见的: 类似p

[Go语言]从Docker源码学习Go——main函数

Go程序从main包下的main函数开始执行,当main执行结束后,程序退出. Docker的main函数在 docker/docker/docker.go package main //Import needed packages; import ( ... "github.com/docker/docker/api" ... ) //define const; const ( defaultCaFile = "ca.pem" defaultKeyFile = &

PHP语言学习之html5的学习,一周总结

通过这周的html5的学习我整理了一下一些东西和一些总结 写代码的时候也开头都要对齐这样开起来很舒服,这些都是用到缩进键搞定的,比如<html></html>在一块写,要前后对齐,这样容易发现错误,而且看起来比较清晰 写代码是还有英文和中文的标点符号的问题,这些都要是注意的,(这基本是我在学习的时候遇到的问题) 要掌握好重要的标签,这样能够更好的学习.... 开始学习html 5 什么是html5HTML是由W3C的维护的 HTML是大小写不敏感的,HTML与html是一样的 HT

[Go语言]从Docker源码学习Go——指针和Structs

这两天在看reflect这个包在Docker中的使用时,遇到了各种问题,最后虽然知道怎么用了. 但是对于这块的原理还不是太懂,于是把"THE WAY TO GO"中关键的几章看了下. 继续坚持往下写,争取能说明白. 源码 还是先看Docker中源码, docker/api/client/cli.go type DockerCli struct { proto string addr string configFile *registry.ConfigFile in io.ReadClo

[Go语言]从Docker源码学习Go——结构和函数的定义

Docker在最近很火,而作为Docker的开发语言-Go也再次被大家提到. 已经使用Docker一段时间了,但是对于源码,尤其是其开发语言Go却一直是一知半解. 最近准备利用空余时间从Docker源代码入手来学习一下Go,同时对Docker的实现也希望可以提高一个层次. 有兴趣的可以一起讨论,学习. 准备工作: 1. Docker源代码https://github.com/docker/docker (版本1.1.2) 2. 安装开发工具LiteIDE, 这个是官方的Go语言的IDE 3. G

李洪强iOS开发之零基础学习iOS开发】【02-C语言】01-概述

前面已经给大家介绍了iOS开发相关的一些基础知识,其实iOS开发就是开发iPhone\iPad上的软件,而要想开发一款软件,首先要学习程序设计语言.iOS开发需要学习的主要程序设计语言有:C语言.C++.Objective-C,其中C++.Objective-C都是以C语言为基础,从C语言衍生出来的.从这讲开始,我们就暂时抛开iOS相关的知识,沉下心来学习传说中的C语言.正式学习之前,先提醒一句:学习一门语言的语法是比较枯燥的事,很像是在学习1+1等于几,不可能说,学习C语言语法过程中就能马上做

mysql学习笔记之六(数据定义语言DDL)

SQL语言包括四类种主要程序设计语言类别的语句:数据定义语言(DDL),数据操作语言(DML)及数据控制语言(DCL)还有事务控制语言(TCL). ※  数据定义语言(DDL),例如:CREATE.DROP.ALTER等语句. ※  数据操作语言(DML),例如:INSERT(插入).UPDATE(修改).DELETE(删除)语句. ※  数据查询语言(DQL),例如:SELECT语句.(一般不会单独归于一类,因为只有一个语句). ※  数据控制语言(DCL),例如:GRANT.REVOKE等语