GDB调试(调试的本质)??栈、堆,虚拟地址布局

本文介绍了调试的基本原则,如从简单工作开始调试、自顶向下方法等,并详细讲解了GDB的使用方法,包括断点设置、查看变量值、多线程调试等高级功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

调试的本质:确认错误的存在。

调试的原则:

1)从简单工作开始调试

(如果代码由大型循环组成,最容易出现错误的是第一次或第二次迭代引发的错误)

2)自顶向下的方法

举例:当遇到对f()的调用时,选择跟在函数调用后的语句;执行调用,然后检查依赖调用结果的变量的值,从而了解该函数是否正确运行。如果正确运行,一般就可避免单步调试。

3)使用调试工具确定段错误位置

发生段错误时,执行的第一步操作应该是在调试器中运行程序并重新产生段错误。调试器将指出发生这种错误的代码行。然后通过调用调试器的反向跟踪获得其他有用信息。

4)通过发出中断确定无限循环的位置

如果怀疑程序有无限循环,进入调试器运行程序,让该程序执行足够长的时间进入循环,然后,使用调试器的中断命令挂起该程序,并且执行反向跟踪,了解到达循环体的位置。

5)使用二分搜索

可用于迭代错误查询(二分迭代次数);用于代码段错误查询(折半删除代码查询)



GDB的 tui模式:

在GDB中使用ctrl+X+A组合键,进入tui模式。

TUI模式中,GDB窗口划分为两个子窗口

ctrl + p和ctrl + N组合键浏览以前GDB命令

使用list更改TUI的源代码子窗体显示代码的区域


基本操作

启动 GDB的方法有以下几种: 

1、gdb <program> 
program也就是你的执行文件,一般在当然目录下。 

(有参数时,set arg XXX)

2、gdb <program> core 
用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生 
的文件。 

3、gdb <program> <PID> 
如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gd 
b会自动attach上去,并调试他。program应该在 PATH环境变量中搜索得到。 

run 运行程序

break 行号 设置断点

break [可选/行号]   if  (条件)   组合命令

break function 在函数function()的入口指定断点

disable breakpoint-list 禁用断点(不带参数,禁用所有断点)

enable breakpoint-list 启用断点(不带参数,启用所有断点)


break filename:line_number 在源代码文件的line_number处设置断点(如果文件不再当前路径则给出全路径)

break filename.cpp:line_number 在源文件中设置断点

tbreak 行号 设置断点(首次到达时起作用)

clear 行号 删除断点(适用于已经到达断点)

delete 断点编号 删除断点、监视点及捕获点

info break 查找断点

next  可选参数 下一/几步

step 可选参数 下一/几步(会进入调用函数体)

finish(简写fin) 恢复执行,直到当前栈帧完成之后止

until(简写u) 恢复执行,退出循环体内部止

continue 恢复执行操作,直到下一个断点

print j 查看变量j的值

print *pointer@number_of_elements 打印动态数组

display 变量 (简写disp)每次有暂停时输出指定条目(适当时候可使用类型强制转换)

info breakpoints 列出所有断点命令

监视点设置

watch z 在变量z值改变时查看

watch (z>18) 通过设置表达式设置监视点

调用栈

frame 1 查看以前的帧(调用的函数的存储)

//执行frame命令时,当前正在执行的函数的帧被编号为0,其父帧编号为1,依次类推。

up 转到下一个父帧

backtrace 命令显示整个栈(当前存在的所有帧的集合)

联机帮助使用:

help breakpoints 显示关于断点的文档

break *address 在虚拟内存地址处设置断点。对于没有调试信息的部分(比如源代码不可用时,或者共享库文件)是必需的。

(缩写,b ->break,cond->condition,r->run,n->next,s->step,c->continue,p->print,bt->backtrace)


GDB启动文件:

默认名为:.gdbinit

启动文件1:放在主目录中,一般

启动文件2:放在包含该项目特有用途的特定目录中

gdb -command=z  x表示要在可执行文件x上运行GDB,首先从文件z中读取命令

启动调试会话:

第一步:break main,在主函数设置断点

第二步:break function,调用函数设置断点,list列出需要看的代码

commands断点命令列表设置

commands 1 执行断点1列表

silent 静默执行

continue 命令列表中最后一个命令时continue,将继续自动执行程序

GDB的define命令创建宏


show user可以得到所有宏的列表

info locals 得到当前栈帧中所有局部变量列表

call printtree(结构体地址) 打印出结构体

x 命令,直接检查给定地址的内存

undisplay 编号 删除显示条目

set 变量=XXX 设置变量

info args检查当前函数参数

方便变量定义: set $gdb变量 = 程序变量



程序崩溃的处理:

访问禁止访问内存->导致segfault(linux,unix)/general protection fault(windows)

Unix平台上,虚拟地址布局:


箭头显示堆和栈的增长方向;

文本区域:由程序源代码中的编译器产生的机器指令组成。例如main()

数据区域:编译时分配的所有程序变量,即全局变量。

      第一个数据子区域:.data,由初始化过的变量组成;

      第二个数据子区域:.bss,未初始化的数据。

堆区域:当动态分配内存时(malloc()和new结构),从堆中分配,堆空间不够通过调用brk()来扩展堆。

栈区域:用来动态分配数据的空间。函数调用的数据(包括参数、局部变量和返回地址)都存储在栈上。每次进行函数调用时栈都会增长,每次返回到调用者,栈都会收缩。

cat 过程号,查看执行程序内存分布


段错误分析查找:

第一步:分析core文件

$ gdb cstring core

第二步:回溯输出

$ backtrace

第三步:把当前帧改为发生错误的帧

$ frame number(帧号)

第四步:输出我们的猜测

$ print XX (变量)


客户端、服务器调试

第一步:确认客户机是否成功连接到了服务器

print function(参数1,参数2...),去掉其中的强制转换,否则会输出错误

第二步:跟踪程序做过的所有系统调用,strace


调试多线程代码

info threads 获得每个线程运行信息(星号表示在当前进程中)

backtrace 查看当前线程在做什么

thread 线程号  切换到其他线程

backtrace 查看其他线程做什么

break Y  thread X  线程X到达源代码Y行时停止

break Y  thread X if i==j  线程X到达源代码Y行,并且变量i 和 j相等时停止执行

(调试多线程需要耐心和创意)


并行应用程序

并行架构:共享内存和消息传递

消息传递程序调试

$ ps ax 确定正在执行应用程序的进程(直接在命令行)

$ gdb 程序名 pid(进程号)   将GDB附加到正在运行的节点上

(gdb) backtrace  查看代码运行到何处

(gdb) frame  #number 移动到问题栈帧处



无法编译或加载调试

1、“幽灵行号”问题

第一步:确认错误代码大概位置,方法:注释掉可能出问题代码;

第二步:恢复代码,重新编译,使错误再次出现;

第三步:二分搜索,反复缩小函数搜索区,直到找到出错点。

2、缺少库

$ ldd 程序 检查程序需要哪些库

第一步:创建或查找所需库文件

第二步:

解决问题方式之一:

添加库坐在搜索路径/Debug/z

%setenv LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:/Debug/z

对于bash,执行如下命令:

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/Debug/z

$ export LD_LIBRARY_PATH

解决开源软件库找不到问题:设置环境变量

% setenv PKG_CONFIG_PATH /usr/lib/pkgconfig:/usr/local/lib/pkgconfig


调试GUI程序

$ tty 查看当前窗口终端号


如何使用splint

splint 有很多开关,可以打开或关闭某个功能(+打开,-关闭)

























<think>首先,用户的问题是关于虚函数表的必要性及其在C++面向对象编程中的作用。用户提供了之前的相关讨论和一些引用,我需要整合这些信息来构建一个全面的回答。###回顾引用内容:-**引用[1]**:-每个包含虚函数的类有一个虚函数表(vtable)。-虚函数表指针(vptr)在对象实例的最前面。-派生类虚表的虚函数地址顺序与基类一致;如果子类重写虚函数,地址改变;如果子类有自己的虚函数,添加在后面。-虚表可以继承:如果未重写,指向基类的实现;重写后指向自身实现。-**引用[2]**:-虚函数存在代码段(codesection),虚表指针存在对象中。-虚表本身存储虚函数指针,不是函数本身。-虚表位置:VS中在代码段,GCC在.rodata(只读数据段)。-**引用[3]**:-静态成员函数不能是虚函数,因为虚函数调用需要通过对象获得vptr,而静态函数可被类名调用。-进一步解释虚函数调用依赖于vptr。-**引用[4]**:-vptr指向虚函数表起始位置。-虚函数表像数组,元素是函数指针,顺序与声明顺序相关。###我的回答应覆盖:1.**虚函数表的必要性**:-C++支持运行时多态(runtimepolymorphism),即通过基类指针或引用调用派生类方法。-实现多态的机制需要一种方式动态解析函数调用,这不能仅靠编译时函数地址绑定。-静态绑定(早绑定)vs.动态绑定(晚绑定):虚函数表支持后者。2.**虚函数表的作用**:-存储虚函数指针:每个类有一个vtable,对象有vptr指向该表。-支持动态调用:当通过基类指针调用虚函数时,运行时通过vptr查找并调用实际函数(派生类版本)。-实现继承和覆盖:虚表继承机制确保派生类可以重写基类虚函数,同时支持自己的虚函数。3.**面向对象多态实现机制**:-多态核心:通过接口(基类)操作对象,实际执行派生类行为。-具体实现:每个对象有vptr指向类的vtable。调用虚函数时:-取对象的前几个字节(vptr)。-通过vptr访问虚函数表。-按顺序获取函数指针并调用。-好处:代码通用性和可扩展性;新增派生类不需修改基类代码。###整合到回答中:-**结构清晰**:-先解释必要性(why)。-再解释作用(whatitdoes)。-接着实现机制(howitworks)。-最后在流程图中可视化。-**参考用户上次问题和我的回答**:用户上一次讨论了抽象类和虚析构函数,所以我需连接多态概念。-**格式要求**:使用$...$和$$...$$;添加引用标识;生成相关问题;中文回答。###回答结构:1.**引言**:简述虚函数表在C++多态中的必要性。2.**必要性分析**:基于引用和知识,说明why。3.**作用描述**:whatitdoes。4.**实现机制**:howitworkswithdiagramsandreferences.5.**流程图**:展示虚函数调用过程。6.**总结**:关键好处。7.**相关问题**:生成几个相关问题。###流程图设计:基于上次回答的流程图,调整焦点到一般虚函数调用:```mermaidgraphTDA[对象实例化]-->B[初始化vptr指向vtable]B-->C[vtable存储虚函数指针数组]C-->D[通过基类指针调用虚函数]D-->E[运行时访问vptr]E-->F[查询vtable]F-->G[获取并执行实际函数指针]```注意:这应简化,专注于调用过程。###引用添加:在回答中自然添加如:虚函数表和vptr机制确保了运行时多态,支持高效和动态函数调度[^1]。###相关问题生成:-1.虚拟继承中虚函数表有何不同?-2.如何避免虚函数表带来的性能开销?-3.C++11中新增的final和override关键字如何影响虚函数表?-基于上下文和引用。</think> ### 虚函数表的必要性及其在C++中的作用 在C++中,虚函数表(**vtable**)是实现面向对象多态的核心机制。以下是其必要性及作用的详细分析: #### 1. **必要性:解决静态绑定的局限** C++支持两种函数绑定方式: - **静态绑定(早绑定)**:编译时确定函数地址,适用于非虚函数。但无法根据对象类型动态调用函数。 - **动态绑定(晚绑定)**:运行时根据对象类型决定调用哪个函数,实现多态。 **关键问题**: 若没有虚函数表,编译器无法为基类指针/引用存储派生类的函数地址。例如: ```cpp class Base { public: void print() { /* 基类实现 */ } // 非虚函数 }; class Derived : public Base { public: void print() { /* 派生类实现 */ } }; int main() { Base* obj = new Derived(); obj->print(); // 编译时静态绑定,调用Base::print(),而非Derived::print() } ``` 结果:行为与对象类型不符,违反面向对象的"开闭原则"(对扩展开放,对修改关闭)[^1]。 #### 2. **虚函数表的作用** 虚函数表通过在运行时动态解析函数地址,解决了上述问题: - **动态多态支持**: 虚表存储类的虚函数指针,允许基类指针调用派生类实际函数。例如: ```cpp class Base { public: virtual void print() { /* 基类实现 */ } // 虚函数 }; ``` 当`Base* obj = new Derived()`时,`obj->print()`通过vtable调用`Derived::print()`[^1]。 - **统一的接口机制**: 虚表标准化了不同类对象的函数调用接口,使代码高度通用。例如,用户只需操作基类指针,就能处理任意派生类对象。 - **安全资源管理**: 虚析构函数依赖虚表确保派生类资源被释放: ```cpp class Base { public: virtual ~Base() {} // 虚析构函数 }; delete obj; // 通过虚表调用实际析构函数链 ``` #### 3. **实现机制:虚函数表的工作原理** 虚表的核心是**对象实例的虚表指针(vptr)**,它指向类的虚函数表。具体结构如下: - **对象内存布局**: ``` +-----------------+ +------------------------+ | 对象实例 |--> | vptr | --→ [虚表地址] |-----------------| +------------------------+ | 其他成员变量 | | 成员变量1 | +-----------------+ | ... | ``` - `vptr`位于对象起始位置(通常占4或8字节)。 - 虚表本身存储函数指针数组,存在于只读数据段(如Linux的`.rodata`或Windows的代码段)[^2]。 - **虚函数调用过程**: ```mermaid graph TD A[通过基类指针调用虚函数] --> B[访问对象的vptr] B --> C[vptr指向虚函数表] C --> D[按索引查找函数指针] D --> E[执行实际函数代码] ``` 示例步骤: - 若`Derived`重写了`Base::print()`,其虚表中`print`的槽位指向`Derived::print()`。 - 若未重写,则指向`Base::print()`[^4]。 - **函数存储区分**: - 虚函数代码位于**代码段**(与其他函数相同)。 - 虚表仅存储指向这些函数的**指针**(非函数本身)[^2]。 #### 4. **在面向对象中的关键作用** - **扩展性**:新增派生类时,只需实现虚函数,无需修改基类。用户代码通过基类接口无缝兼容新类。 - **多态类型安全**:确保对象行为与其类型一致,支持如工厂模式、策略模式等设计模式。 - **性能权衡**: - 优点:运行时动态调度灵活。 - 开销:每次虚函数调用增加一次指针跳转(纳秒级),虚表占用内存(通常1个指针/虚函数)[^4]。 --- ### 总结:虚函数表在C++中的核心价值 - **必要性**:唯一支持动态多态的机制,避免对象行为与类型不匹配的问题。 - **作用**: - 通过`vptr`和虚表实现运行时函数调度。 - 统一多态接口,提升代码可复用性和可扩展性。 - 保障资源安全(结合虚析构函数)。 - **优势**:在空间和时间开销可控的前提下,为面向对象编程提供本质支持。 ### 相关问题 1. 虚拟继承如何影响虚函数表的布局? 2. 在什么场景下虚函数表可能被破坏或出现非法访问? 3. C++11的`final`和`override`关键字如何优化虚函数表的使用? 4. 如何通过调试工具(如GDB)观察内存中的虚函数表结构?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值