0
点赞
收藏
分享

微信扫一扫

gdb基本使用介绍

GDB介绍

GDB是GNU Debugger的简称,其作用是可以在程序运行时,检测程序正在做什么。GDB程序自身是使用C/C++程序编写的,但可以支持除C/Cpp之外很多编程语言的调试。GDB原生支持调试的语言包含:C/Cpp/D/Go/Object-C/OpenCL C/Fortran/Rust等等。

使用GDB,我们可以方便地进行如下任务:

  1. 如果程序崩溃后产生了core dump文件,gdb可以通过分析core dump文件,找到程序crash的位置,调用堆栈等用于找到问题原因的关键信息;
  2. 在程序运行时,GDB可以检测当前检测程序正在干什么事情;
  3. 在程序运行的时,修改变量的值,这个对于提高工作效率很有用处;
  4. 可以使的程序在特定条件下中断;
  5. 监视内存地址变动;

GDB不适合做什么事情:

  1. GDB可以用来辅助调试内存泄露问题,但GDB不能用于内存泄露检测;
  2. GDB可以用来辅助程序性能调优,但是GDB不能用来程序性能问题分析;
  3. GDB不是编译器,不能运行有编译问题的程序,也不能用来调试编译问题;

GDB使了解三方中间件、无源码程序、解决程序疑难杂症的利器。GDB有着日志无法比拟的优势,此外,GDB还非常适合对多种开发语言混合的程序进行调试。

案例实操汇总

  1. Segmentation Fualt问题排查
  2. 程序阻塞问题排查
  3. 数据纂改问题排查
  4. 堆内存重复释放问题排查

++Segmentation Fault问题排查++ 定义:Segmentation Fault是进程访问了由操作系统内存保护机制规定的受限的内存区域触发的。当发生Segmentation Fault异常时,操作系统通过发起一个“SIGSEGV”信号来终止进程,而且,Segmentation Fault不能被异常捕获代码捕获,是导致程序Crash的常见诱因。 对于C/Cpp等贴近操作系统的开发语言,由于提供了灵活的内存访问机制,所以自然成为了Segmentation Fault异常的重灾区,由于默认的Segmentation Fault异常几乎没有详细的错误信息,所以开发人员处理此类异常时变得更为棘手。 范畴:在实际开发中,使用了未初始化的指针,空指针,已经被回收的内存指针,栈/堆溢出(越界)等方面,都会引发Segmentation Fault。 案例: simulateSegmentationFault.cpp

#include <iostream>
class Employee{
public:
std::string name;
};
void simulateSegmentationFault(const std::string& name) {
try {
Employee *employee = new Employee();
employee->name = name;
std::cout << Employee name is << employee->name << std::endl;
delete employee;
std::cout << After deletion, employee name is << employee->name << std::endl;
} catch(...) {
std::cout << Error occurred! << std::endl;
}
}

int main (int argc, char **argv) {
std::string text = Hello world;
std::cout << text << std::endl;
simulateSegmentationFault(text);
return 0;
}

编译程序并运行程序,如下图: image.png 从结果上来看,我们的异常捕获代码对于Segmentation Fault无能为力。其次,发生异常时没有打印任何对我们有帮助的提示信息。 成功加载core文件后,我们首先使用bt命令来查看Crash位置的错误堆栈。从堆栈信息中,可以看到__GI__IO_fwrite方法的buf参数的值是0x0,这显然不是一个合法的数值。序号为5的栈帧,是发生异常前,我们自己的代码压入的最后一个栈帧,信息中甚至给出了发生问题时的调用位置在simulateSegmentationFault.cpp 文件的第13行(simulateSegmentationFault.cpp:13),我们使用up 3 命令向前移动3个栈帧,使得当前处理的栈帧移动到编码为3的栈帧。 image.png 此时可以看到传入的参数name是没有问题的,使得list命令查看下问题调用部分的上下文,再使用info locals命令查看调用时的局部变量的情况。最后使用p *employee命令,查看employee指针指向的数据。 image.png 此时可以看到第13行,使用std::cout输出Employee的name属性时,employee指针指向的地址的name属性已经是一个无效地址了(0x0)。 ++程序阻塞问题排查++ 范围:程序阻塞一般包括:

  • 并发程序中产生了死锁,线程无法获取到锁对象
  • 远程调用长时间阻塞无法返回
  • 程序长时间等待某个时间通知
  • 程序产生了死循环
  • 访问了受限的资源和IO,处于排队阻塞状态 对于大多数阻塞来说,被阻塞的线程会处于休眠状态,放置在等待队列,并不会占用系统的CPU时间。但是如果这种行为不符合程序的预期,那么我们就需要查明程序当前在等待哪一个锁对象,程序阻塞在哪个方法,程序在访问哪个资源时卡住了等问题。 案例:
#include <thread>
#include <mutex>
#include <iostream>

std::mutex my_mu;
void thread1_func() {
for (int i = 0; i < 5; ++i) {
my_mu.lock();
std::cout << thread1 lock mutex succeed! << std::endl;
std::this_thread::yield();
}
}

void thread2_func() {
for (int i = 0; i < 5; ++i) {
my_mu.unlock();
std::cout << thread2 unlock mutex succeed! << std::endl;
std::this_thread::yield();
}
}

void simulateBlocking() {
std::thread thread1(thread1_func);
std::thread thread2(thread2_func);
thread1.join();
thread2.join();
}
int main (int argc, char **argv) {
std::string text = Hello world;
std::cout << text << std::endl;
simulateBlocking();
return 0;
}

编译并运行程序,如下图: image.png 为了调查程序阻塞的原因,我们使用命令把gdb关联到运行中的进程;步骤如下: 1. gdb simulationBlocking,运行bt查看堆栈; 2. 直接跳转到f 2,查看代码list;此时我们通过查看堆栈信息,知道阻塞的位置是在simulateBlocking.cpp的31行,即thread1.join()并没有完成,继续往下面查; image.png 3. 执行info threads 查看所有运行的线程; 4. 查看编号为2的线程的堆栈:thread apply 2 bt;切换到线程2:thread 2; 5. 查看thread1的堆栈:bt;直接条找到我们代码所在的栈帧:f 4; 6. 查看锁对象: p my_mu; 7. 确认持有锁的线程:info threads; image.png ++数据篡改问题排查++ 定义:数据被篡改不一定会引起异常,但很有可能会导致业务结果不符合预期,对于大量使用了三方库的项目来说,想知道数据在哪里被修改成了什么,并不是一件容易的事情。对于C/Cpp来说,还存在着指针被修改后,导致指针原来指向的对象可能无法回收的问题。单纯从日志来看,很难发现一个变量何时被哪个变量修改成了什么,所以需要借助GDB监控断点。 案例: simulateDataChanged.cpp

#include <thread>
#include <mutex>
#include<syscall.h>
#include<sys/types.h>
#include<unistd.h>
#include <iostream>

std::mutex my_mu;

class Employee{
public:
Employee(const std::string& name) {_name = name;}
std::string _name;
};

pid_t gettid(){
return static_cast<pid_t>(syscall(SYS_gettid));
}

void check_func(Employee& e) {
auto tid = gettid();
std::cout << thread << tid << stated. << std::endl;
while(true) {
if (e._name.compare(origin employee name) != 0) {
std::cout << Error occurred, Employee name changed, new value is: << e._name << std::endl;
break;
}
}
std::this_thread::yield();
}

void modify_func(Employee& e) {
auto tid = gettid();
std::cout << thread << tid << started. << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(0));
e._name = std::string(employee name changed);
}

void simulateDataChanged() {
Employee e(Origin employee name);
std::thread thread1(check_func, std::ref(e));
std::thread thread2(modify_func, std::ref(e));
thread1.join();
thread2.join();
}
int main (int argc, char **argv) {
std::string text = Hello world;
std::cout << text << std::endl;
simulateDataChanged();
return 0;
}

编译,运行程序; gdb调试,步骤如下: 1. gdb simulateDataChanged 2. 在simulateDataChanged方法上添加断点,b simulateDataChanged.cpp:simulateDataChanged运行程序 r; 3. 使得程序执行到e对象创建完成之后,执行两次n; 4. 监视e._name:watch -location e._name; 5. 继续执行c,在触发了watch中断后,查看终端所在位置的堆栈,bt; 6. 直接跳转到我们的代码所在的栈帧, f 2; image.png ++堆内存重复释放问题排查++ 定义:堆内存的重复释放,会导致内存泄露,被破坏的内存可以被攻击者利用,从而产生更为严重的安全问题。目标流行的C函数库(比如libc),会在内存重复释放时,抛出“double free or corruption (fasttop)”错误,并终止程序运行。为了修复堆内存重复释放问题,我们需要找到所有释放对应堆内存的代码位置,用来判断哪一个释放堆内存的操作是不正确的。

使用GDB可以解决我们知道哪一个变量产生了内存重复释放,但我们不知道都在哪里对此变量释放了内存空间的问题。如果我们对产生内存重复释放问题的变量一无所知,那么还需要借助其它的工具来辅助定位。

下面我们使用两个线程,在其中释放同一块堆内存,用来模拟堆内存重复释放问题; 案例: simulateDoubleFree.cpp

#include <thread>
#include <mutex>
#include<syscall.h>
#include<sys/types.h>
#include<unistd.h>
#include <iostream>

std::mutex my_mu;

class Employee{
public:
Employee(const std::string& name) {_name = name;}
std::string _name;
};

pid_t gettid(){
return static_cast<pid_t>(syscall(SYS_gettid));
}

void thread1_func(Employee* e) {
auto tid = gettid();
std::cout << thread << tid << stated. << std::endl;
e->_name = new employee name1;
delete e;
e = nullptr;
}

void thread2_func(Employee* e) {
auto tid = gettid();
std::cout << thread << tid << started. << std::endl;
e->_name = new employee name2;
delete e;
e = nullptr;
}

void simulateDoubleFree() {
Employee *e = new Employee(Origin employee name);
std::thread thread1(thread1_func, e);
std::thread thread2(thread2_func, e);
thread1.join();
thread2.join();
}
int main (int argc, char **argv) {
std::string text = Hello world;
std::cout << text << std::endl;
simulateDoubleFree();
return 0;
}

编译并运行程序,如下图: image.png gdb调试,步骤如下: 1. 首先在创建好e变量之后的地方,加入断点:b simulateDoubleFree.cpp:38,随后执行 r; 2. 创建好变量e之后,再执行 p &e,获取到e的指针地址, 随后执行bt; 3. 之后在所有free e内存的地方,加断点,b __GI_libc_free if mem == 0x7fffffffdb68,随后运行c;执行bt就可以看到第一个free e的地方;如下图: image.png 4. 继续执行c和bt可以看到第二个free e的地方。如下图: image.png 现在我们使用GDB来找到所有释放employee变量堆内存的代码位置,以便决定哪个释放操作不是需要的:

常用的GDB命令

image.png image.png

举报

相关推荐

0 条评论