Linux C编程(五) 之 gdb详解

GDB调试

gdb是gnu项目组开发的一款Unix下功能强大的调试器 主要用来调试C/C++程序

  • 基于命令行的调试方法 可以随心所欲的运行程序
  • 所有的调试都是可以进行脚本编写的
  • 能够调试所有架构的代码
  • 有三种调试方法供大家选择 可安装插件
  • gdb 可以下多种类型的断点
  • GDB支持远程调试,支持与IDA进行联调
  • 可动态改变程序的运行环境
gdb调试前提

若想使用gdb进行程序调试,需要在编译时将调试信息添加在可执行文件里 即cc/gcc/g++编译工具链的-g 选项

root@ubuntu:/work/C/day09# gcc -g -o hello hello.c 
root@ubuntu:/work/C/day09# ls
hello  hello.c

gdb插件安装

为了杜绝原始gdb使用的无色彩 功能略小带来的晦涩感 特推荐进行以下gdb插件的安装

安装gdb增强工具
  • GDB的版本大于7.7
  • wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh
  • 确保网络连通 并且成功更新ubuntu (更新source.list 使用apt-get update)

GDB安装插件

git clone https://github.com/gatieme/GdbPlugins.git ~/GdbPlugins
echo "source ~/GdbPlugins/peda/peda.py" > ~/.gdbinit --->破解 逆向
echo "source ~/GdbPlugins/gef/gef.py" > ~/.gdbinit ----> debug 逆向
echo "source ~/GdbPlugins/gdbinit/gdbinit" > ~/.gdbinit ---> 个人定制
  • 解释 : 在终端执行三条echo语句中的一句 可切换至相应的gdb插件版本
  • 下面例子中 采取debug版本 即第二种调试插件
gdb调试程序的几种启动方式
  • 本地普通启动 gdb

  • 本地段错误文件启动 gdb core

  • attch方式启动 gdb

  • 远程启动 gdbserver 0.0.0.0:1234 /path/to/file

  • GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb -help查看。我在下面只例举一些比较常用的参数:

     -symbols  
      -s  
    

    从指定文件中读取符号表。

      -se file 
    

    从指定文件中读取符号表信息,并把他用在可执行文件中。

     	 -core 
    	  -c  
    

    调试时core dump的core文件。

      -directory 
      -d 
    

    加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。

      示例程序:
      #define MIN(a,b)   (a)>(b)?(b):(a)
      #include
      int fun(int a,int b,int c)
      {
      	return MIN(MIN(a,b),c);
      }
      int main()
      {
      	int a,b,c;
      	int min;
      	a=5;
      	b=9;
      	c=4;
      	min=fun(a,b,c);
      	printf("min=%d\n",min);
      	return 0;
      }
    
  • 启动gdb
    Linux C编程(五) 之 gdb详解_第1张图片
    命令:

  1. l list 显示程序 默认显示10行
  2. set listsize 20 将显示行数设置为20
  3. 直接回车 执行上一次命令
  4. b break 设置断点
  5. b 10设置断点,在源程序第10行
  6. b func设置断点,在func函数入口处
  7. bt 查看函数调用栈
  8. r run 运行程序
  9. cd 相当于shell的cd命令。
  10. pwd 显示当前的所在目录。
  11. info b [n] 查看断点信息
  12. shell ls 执行shell命令ls
  13. next 单步跟踪,函数调用当作一条简单语句执行,可简写为n
  14. step 单步跟踪,函数调进入被调用函数体内,可简写为s
  15. finish 退出函数
  16. until 在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体,可简写为u。
  17. continue 继续运行程序,可简写为c
  18. stepi或si, nexti或ni 单步跟踪一条机器指令,一条程序代码有可能由数条机器指令完成,stepi和nexti可以单步执行机器指令。
  19. info program 来查看程序的是否在运行,进程号,被暂停的原因。
  20. print 打印变量、字符串、表达式等的值,可简写为p
    p count 打印count的值
    p cou1+cou2+cou3 打印表达式值
    print接受一个表达式,GDB会根据当前的程序运行的数据来计算这个表达式,表达式可以是当前程序运行中的const常量、变量、函数等内容。但是GDB不能使用程序中定义的宏。
  21. up //上移栈帧,使另一函数成为当前函数
  22. watch //在程序中设置一个监测点(即数据断点)
  23. whatis //显示变量或函数类型
  24. quit 退出gdb
  25. 快捷命令方式
  • 敲入b按两次TAB键,你会看到所有b打头的命令:

       (gdb) b
        backtrace  break      bt
        (gdb)
    
  • 只记得函数的前缀,可以这样:

       (gdb) b make_ <按TAB键>
       (再按一次TAB键,你会看到:)
       make_a_section_from_file     make_environ
       make_abs_section             make_function_type
       make_blockvector             make_pointer_type
       make_cleanup                 make_reference_type
       make_command                 make_symbol_completion_list
       (gdb) b make_
    
GDB把所有make开头的函数全部例出来给你查看。
示例四:调试C++的程序时,有可以函数名一样。如:
(gdb) b 'bubble( M-? 
bubble(double,double)    bubble(int,int)
(gdb) b 'bubble(
你可以查看到C++中的所有的重载函数及参数。(注:M-?和“按两次TAB键”是一个意思)

设置断点(BreakPoint)

我们用break命令来设置断点。正面有几点设置断点的方法:

break  
  • 在进入指定函数时停住。C++中可以使用class::function或function(type,type)格式来指定函数名。

  • 在指定行号停住。

      break +offset 
      break -offset 
    
  • 在当前行号的前面或后面的offset行停住。offiset为自然数。

      break filename:linenum 
          在源文件filename的linenum行处停住。
      break filename:function 
          在源文件filename的function函数的入口处停住。
      break *address
          在程序运行的内存地址处停住。
      break 
          break命令没有参数时,表示在下一条指令处停住。
      break ... if 
          ...可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环境体中,可以设置break if i=100,表示当i为100时停住程序。
      查看断点时,可使用info命令,如下所示:(注:n表示断点号)
      info breakpoints [n] 
      info break [n] 
    
设置观察点(WatchPoint)
  • 观察点一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。我们有下面的几种方法来设置观察点:

     watch 
         为表达式(变量)expr设置一个观察点。一量表达式值有变化时,马上停住程序。
         
     rwatch 
         当表达式(变量)expr被读时,停住程序。
         
     awatch 
         当表达式(变量)的值被读或被写时,停住程序。
     
     info watchpoints
         列出当前所设置了的所有观察点。
    
设置捕捉点(CatchPoint)

你可设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是C++的异常。设置捕捉点的格式为:

catch 

当event发生时,停住程序。event可以是下面的内容:

  1. throw 一个C++抛出的异常。(throw为关键字)
  2. catch 一个C++捕捉到的异常。(catch为关键字)
  3. exec 调用系统调用exec时。(exec为关键字,目前此功能只在HP-UX下有用)
  4. fork 调用系统调用fork时。(fork为关键字,目前此功能只在HP-UX下有用)
  5. vfork 调用系统调用vfork时。(vfork为关键字,目前此功能只在HP-UX下有用)
  6. load 或 load 载入共享库(动态链接库)时。(load为关键字,目前此功能只在HP-UX下有用)
  7. unload 或 unload 卸载共享库(动态链接库)时。(unload为关键字,目前此功能只在HP-UX下有用)
  8. tcatch
    只设置一次捕捉点,当程序停住以后,应点被自动删除。
程序运行参数

set args 可指定运行时参数。(如:set args 10 20 30 40 50 )
show args 命令可以查看设置好的运行参数。
run ® 启动程序
不指定运行参数 r
指定运行参数r 10 20 30 40 50

程序变量

在GDB中,你可以随时查看以下三种变量的值:

  1. 全局变量(所有文件可见的)

  2. 静态全局变量(当前文件可见的)

  3. 局部变量(当前Scope可见的)
    如果你的局部变量和全局变量发生冲突(也就是重名),一般情况下是局部变量会隐藏全局变量,也就是说,如果一个全局变量和一个函数中的局部变量同名时,如果当前停止点在函数中,用print显示出的变量的值会是函数中的局部变量的值。如果此时你想查看全局变量的值时,你可以使用“::”操作符:
    file::variable
    function::variable
    可以通过这种形式指定你所想查看的变量,是哪个文件中的或是哪个函数中的。例如,查看文件f2.c中的全局变量x的值:

     p ‘f2.c’::x
    

当然,“::”操作符会和C++中的发生冲突,GDB能自动识别“::”是否C++的操作符,所以你不必担心在调试C++程序时会出现异常。

  • x/n、f、u
    n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
    f 表示显示的格式,跟print 的格式参数相同
    u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。
数组变量

有时候,你需要查看一段连续的内存空间的值。比如数组的一段,或是动态分配的数据的大小。你可以使用GDB的“@”操作符,“@”的左边是第一个内存的地址的值,“@”的右边则你你想查看内存的长度。例如,你的程序中有这样的语句:

int *array = (int *) malloc (len * sizeof (int));

于是,在GDB调试过程中,你可以以如下命令显示出这个动态数组的取值:

p *array@len

@的左边是数组的首地址的值,也就是变量array所指向的内容,右边则是数据的长度,其保存在变量len中。

display自动显示

你可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display。

display expr
display/fmt expr
display/fmt addr

expr是一个表达式,fmt表示显示的格式,addr表示内存地址,当你用display设定好了一个或多个表达式后,只要你的程序被停下来,GDB会自动显示你所设置的这些表达式的值。

info display   查看display设置的自动显示的信息。
undisplay dnums…
delete display dnums…

删除自动显示,dnums意为所设置好了的自动显式的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5)

disable display dnums…
enable display dnums…
disable和enalbe不删除自动显示的设置,而只是让其失效和恢复。
历史记录

当你用GDB的print查看程序运行时的数据时,你每一个print都会被GDB记录下来。GDB会以1,2, 3……这样的方式为你每一个print命令编上号。于是,你可以使用这个编号访问以前的表达式,如1。这个功能所带来的好处是,如果你先前输入了一个比较长的表达式,如果你还想查看这个表达式的值,你可以使用历史记录来访问,省去了重复输入。

(gdb)show values
Print the last ten values in the value history, with their item numbers. This is
like ‘p $$9’ repeated ten times, except that show values does not change the
history.

(gdb)show values n
Print ten history values centered on history item number n.

(gdb)show values +
Print ten history values just after the values last printed. If no more values are
available, show values + produces no display.

改变程序的执行

一旦使用GDB挂上被调试程序,当程序运行起来后,你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值,这个强大的功能能够让你更好的调试你的程序,比如,你可以在程序的一次运行中走遍程序的所有分支。

  1. 修改变量值
    修改被调试程序运行时的变量值,在GDB中很容易实现,使用GDB的print命令即可完成。如:

     (gdb) print x=4
    

x=4这个表达式是C/C++的语法,意为把变量x的值修改为4,如果你当前调试的语言是Pascal,那么你可以使用Pascal的语法:x:=4。
在某些时候,很有可能你的变量和GDB中的参数冲突,如:

(gdb) whatis width
type = double
(gdb) p width
$4 = 13
(gdb) set width=47
Invalid syntax in expression.

因为,set width是GDB的命令,所以,出现了“Invalid syntax in expression”的设置错误,此时,你可以使用set var命令来告诉GDB,width不是你GDB的参数,而是程序的变量名,如:

(gdb) set var width=47

另外,还可能有些情况,GDB并不报告这种错误,所以保险起见,在你改变程序变量取值时,最好都使用set var格式的GDB命令。

  1. 跳转执行
    一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由GDB的jump命令来完:

     jump linespec
    

指定下一条语句的运行点。可以是文件的行号,可以是file:line格式,可以是+num这种偏移量格式。表示下一条运行语句从哪里开始。

	jump *address

这里的是代码行的内存地址。
注意,jump命令不会改变当前的程序栈中的内容,所以,当你从一个函数跳到另一个函数时,当函数运行完返回时进行弹栈操作时必然会发生错误,可能结果还是非常奇怪的,甚至于产生程序Core Dump。所以最好是同一个函数中进行跳转。
熟悉汇编的人都知道,程序运行时,eip寄存器用于保存当前代码所在的内存地址。所以,jump命令也就是改变了这个寄存器中的值。于是,你可以使用“set $pc”来更改跳转执行的地址。如:

set $pc = 0×485
  1. 产生信号量
    使用singal命令,可以产生一个信号量给被调试的程序。如:中断信号Ctrl+C。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号非常有利程序的调试。
    语法是:

     signal signal
    

UNIX的系统信号量通常从1到15。所以取值也在这个范围。
single命令和shell的kill命令不同,系统的kill命令发信号给被调试程序时,是由GDB截获的,而single命令所发出一信号则是直接发给被调试程序的。

  1. 强制函数返回
    如果你的调试断点在某个函数中,并还有语句没有执行完。你可以使用return命令强制函数忽略还没有执行的语句并返回。

     return
     return expression
    

使用return命令取消当前函数的执行,并立即返回,如果指定了,那么该表达式的值会被认作函数的返回值。

  1. 强制调用函数

     call expr
    

表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是void,那么就不显示。

	print expr

另一个相似的命令也可以完成这一功能——print,print后面可以跟表达式,所以也可以用他来调用函数,print和call的不同是,如果函数返回void,call则不显示,print则显示函数返回值,并把该值存入历史数据中。

运行环境。
path  可设定程序的运行路径。
show paths 查看程序的运行路径。
set environment varname [=value] 设置环境变量。如:set env USER=hchen
show environment [varname] 查看环境变量。

查看栈信息

当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的。当你的程序调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入“栈”(Stack)中。你可以用GDB命令来查看当前的栈中的信息。

下面是一些查看函数调用栈信息的GDB命令:

breacktrace,简称bt

打印当前的函数调用栈的所有信息。如:

(gdb) bt

#0 func (n=250) at tst.c:6

#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30

#2 0x400409ed in __libc_start_main () from /lib/libc.so.6

从上可以看出函数的调用栈信息:__libc_start_main –> main() –> func()

backtrace n

bt n

n是一个正整数,表示只打印栈顶上n层的栈信息。

backtrace -n

bt -n

-n表一个负整数,表示只打印栈底下n层的栈信息。

如果你要查看某一层的信息,你需要在切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的是切换当前栈。

frame n

n是一个从0开始的整数,是栈中的层编号。比如:frame 0,表示栈顶,frame 1,表示栈的第二层。

frame addr

f addr Select the frame at address addr. This is useful mainly if the chaining of stack frames has been damaged by a bug, making it impossible for gdb to assign

numbers properly to all frames. In addition, this can be useful when your program has multiple stacks and switches between them.

up n

表示向栈的上面移动n层,可以不打n,表示向上移动一层。

down n

表示向栈的下面移动n层,可以不打n,表示向下移动一层。

上面的命令,都会打印出移动到的栈层的信息。如果你不想让其打出信息。你可以使用这三个命令:

select-frame 对应于 frame 命令。

up-silently n 对应于 up 命令。

down-silently n 对应于 down 命令。

查看当前栈层的信息,你可以用以下GDB命令:

frame 或 f

会打印出这些信息:栈的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。

info frame

info f

反调试技术

gcc -o elfa elf32_main.c

多进程调试

  1. 确定gdb中的进程跟踪模式

     	show follow-fork-mode
     	show detach-on-fork 
     	follow-fork-mode  detach-on-fork
     		parent			 on				只调试父进程,子进程正常运行
     		child	   		 on			  	只调试子进程,父进程正常运行
     		parent			 off			同时调试两个进程,子进程暂停在fork位置
     		child			 off			同时调试两个进程,父进程暂停在fork位置
    
  2. 进程间的切换

     info inferiors
     inferiors num	
     add-inferior [-copies n] [-exec executable]
     detach inferiors processid     
     detach 一个由指定的进程,然后从fork 列表里删除。这个进程会被允许继续独立运行。
     kill inferiors  processid  杀死一个由指定的进程,然后从fork 列表里删除。       		
     catch fork  让程序在fork,vfork或者exec调用的时候中断
    

多线程调试

  • GDB帮你决定现在的调试走向
    info threads 正在运行的线程信息
    b … thread …
    b line thread threadno if…
  • 所有线程会因为一个断点所中断

gcc -g thread.c -o thread -lpthread

set non-stop on/off: 
当调式一个线程时,其他线程是否运行。
set pagination on/off: 
在使用backtrace时,在分页时是否停止。
set target-async on/ff: 
同步和异步。同步,gdb在输出提示符之前等待程序报告一些线程已经终止的信息。而异步的则是直接返回。
show scheduler-locking: 
查看当前锁定线程的模式
	set scheduler-locking off|on|step 
		off 不锁定任何线程 
		on 锁定其他线程,只有当前线程执行
	
info threads 查询线程信息 
thread threadno 切换线程
thread apply [threadno] [all] args 对线程列表执行命令
set print thread-events 控制线程开始和结束时的打印信息
show print thread-events 显示线程打印信息的开关状态

**

多进程多线程调试重在控制进程/线程的切换 和对一个线程或进程调试时对其他线程或进程的影响

**

Linux程序发布流程

  • 确定程序是否存在符号表
    readelf -s test-1
  • 生成符号表
    objcopy --only-keep-debug test-1 test-1.symbol
  • 生成发布程序
    objcopy --strip-debug test-1 test-release
  • 使用符号表进行程序debug
    gdb -q --symbol=test-1.symbol --exec=test-release

暂时就先写这么多吧,以后遇到再和大家分享,有不足之处欢迎指出。

你可能感兴趣的:(Linux C编程(五) 之 gdb详解)