6.10. Working with C++

6.10. Working with C++

C++ is a superset of C and includes some new concepts such as classes with constructors and destructors, inline functions, and exceptions. While the full list is beyond the scope of this chapter, we will outline a few of the differences and how to deal with them in GDB.

c++ 是 c 的超集, 包括一些新概念, 如具有构造函数和析构函数的类、内联功能和异常。虽然完整的列表超出本章的范围, 但我们将概述一些差异以及如何在 GDB 中处理它们。

6.10.1. Global Constructors and Destructors

There are a few ways to handle global constructors and destructors. Constructors are run as part of an object initialization. This can occur even before the main() function is called. Destructors are run when an object is destroyed, which can occur after the main() function exits. If you know the constructor or destructor name, you can set a breakpoint in it. But what if you don’t know which constructors or destructors an executable or library has?

有几种方法可以处理全局构造函数和析构函数。构造函数作为对象初始化的一部分运行。即使在调用 main () 函数之前, 构造函数也可能运行。析构函数在对象被销毁时运行, 在main () 函数退出后可能运行。如果知道构造函数或析构函数名称, 则可以在其中设置断点。但是, 如果您不知道可执行文件或库的构造函数或析构函数有哪些,那该怎么办呢?

There is a relatively simple set of steps that you can use to find the global constructors and destructors. The following example shows these steps to find a constructor.

有一组相对简单的步骤可用于查找全局构造函数和析构函数。下面的示例演示了查找构造函数的步骤。

Consider a simple program that has a class and defines a global object from that class. For example, here is a code snippet that includes a simple class, myClass, and global object, myObj2, created with the class as its type.

请考虑一个具有类的简单程序, 并从该类定义一个全局对象。例如, 下面是一个代码段, 其中包含一个简单类,myClass 和全局对象 myObj2。

#include <stdio.h>

 

class myClass

{

  public:

 

  int myVar ;

  myClass() {

   myVar = 5 ;

  }

 

};

 

myClass myObj2 ;

 

When the object, myObj2, is instantiated, the constructor will be run to perform any initialization for the object. For now, we’ll assume that we don’t know about this global object and will try to find it using GDB.

当实例化对象 myObj2 时, 将运行构造函数来执行对象的初始化。现在, 假设我们不知道这个全局对象, 并且将尝试使用 GDB 来查找它。

The first step is to disassemble the _init function. This function is run when a library is loaded or when a program is first executed.

第一步是反汇编 _init 函数。此函数在加载库或首次执行程序时运行。

(gdb) disass _init

Dump of assembler code for function _init:

0x8048300 <_init>:   push  %ebp

0x8048301 <_init+1>:  mov  %esp,%ebp

0x8048303 <_init+3>:  sub  $0x8,%esp

0x8048306 <_init+6>:  call  0x8048384 <call_gmon_start>

0x804830b <_init+11>:  nop

0x804830c <_init+12>:  call  0x80483f0 <frame_dummy>

0x8048311 <_init+17>:  call  0x8048550 <__do_global_ctors_aux>

0x8048316 <_init+22>:  leave

0x8048317 <_init+23>:  ret

End of assembler dump.

(gdb)

 

In the disassembly listing, we can clearly see a call to __do_global_ctors_aux. Now let’s disassemble that function:

在汇编程序中, 我们可以清楚地看到__do_global_ctors_aux。现在, 让我们反汇编该函数:

Code View: Scroll / Show All

(gdb) disass __do_global_ctors_aux

Dump of assembler code for function __do_global_ctors_aux:

0x8048550 <__do_global_ctors_aux>:    push  %ebp

0x8048551 <__do_global_ctors_aux+1>:  mov  %esp,%ebp

0x8048553 <__do_global_ctors_aux+3>:  push  %ebx

0x8048554 <__do_global_ctors_aux+4>:  push  %edx

0x8048555 <__do_global_ctors_aux+5>:  mov  0x80497fc,%eax

0x804855a <__do_global_ctors_aux+10>: cmp  $0xffffffff,%eax

0x804855d <__do_global_ctors_aux+13>: mov  $0x80497fc,%ebx

0x8048562 <__do_global_ctors_aux+18>: je   0x804857c

<__do_global_ctors_aux+44>

0x8048564 <__do_global_ctors_aux+20>: lea  0x0(%esi),%esi

0x804856a <__do_global_ctors_aux+26>: lea  0x0(%edi),%edi

0x8048570 <__do_global_ctors_aux+32>: sub  $0x4,%ebx

0x8048573 <__do_global_ctors_aux+35>: call  *%eax

0x8048575 <__do_global_ctors_aux+37>: mov  (%ebx),%eax

0x8048577 <__do_global_ctors_aux+39>: cmp  $0xffffffff,%eax

0x804857a <__do_global_ctors_aux+42>: jne  0x8048570

<__do_global_ctors_aux+32>

0x804857c <__do_global_ctors_aux+44>: pop  %eax

0x804857d <__do_global_ctors_aux+45>: pop  %ebx

0x804857e <__do_global_ctors_aux+46>: pop  %ebp

0x804857f <__do_global_ctors_aux+47>: ret

End of assembler dump.

(gdb)

 

The previous assembly listing shows the following call *%eax call. This takes the value of the EAX register, treats it as an address, dereferences the address, and the calls function stored at the dereferenced address. From code above this call, we can see EAX being set with mov 0x80497fc,%eax. Let’s take a look at what is at that address:

上一个汇编程序显示 *%eax调用。这将获取 EAX 寄存器的值, 将其视为地址、取消引用地址以及调用存储在取消引用地址上的函数。从上面的代码调用, 我们可以看到用mov 0x80497fc,%eax 设置EAX。让我们来看看这个地址是什么:

Code View: Scroll / Show All

 (gdb) x/40x 0x80497fc

0x80497fc <__CTOR_LIST__+4>: 0x0804851e     0x00000000     0xffffffff 0x00000000

0x804980c <__JCR_LIST__>:    0x00000000     0x08049718     0x00000000 0x00000000

0x804981c  <_GLOBAL_OFFSET_TABLE_+12>:    0x0804832e      0x0804833e 0x0804834e   0x00000000

0x804982c <completed.1>:     0x00000000    0x00000000      0x00000000 0x00000000

0x804983c:   Cannot access memory at address 0x804983c

 

That address is four bytes past the start of a variable called __CTOR_LIST__(we know this from the text, <__CTOR_LIST__+4>). There is a value 0x0804851e at this address, which according to the preceding assembly language, is the value that is called after dereferencing EAX. Let’s see what’s at that address:

该地址是一个名为 __CTOR_LIST__ 的变量的开头的四个字节 (我们从文本<__CTOR_LIST__+4> 中知道这一点)。在这个地址上有一个值 0x0804851e, 根据前面的汇编语言, 是在取消引用 EAX 后调用的值。让我们看看这个地址是什么:

(gdb) disass 0x0804851e

Dump of assembler code for function _GLOBAL__I_myObj2:

0x804851e <_GLOBAL__I_myObj2>: push  %ebp

0x804851f <_GLOBAL__I_myObj2+1>:    mov  %esp,%ebp

0x8048521 <_GLOBAL__I_myObj2+3>:    sub  $0x8,%esp

0x8048524 <_GLOBAL__I_myObj2+6>:    sub  $0x8,%esp

0x8048527 <_GLOBAL__I_myObj2+9>:    push  $0xffff

0x804852c <_GLOBAL__I_myObj2+14>:    push  $0x1

0x804852e    <_GLOBAL__I_myObj2+16>:             call   0x80484d8

<__static_initialization_and_destruction_0>

0x8048533 <_GLOBAL__I_myObj2+21>:    add  $0x10,%esp

0x8048536 <_GLOBAL__I_myObj2+24>:    leave

0x8048537 <_GLOBAL__I_myObj2+25>:    ret

End of assembler dump.

(gdb)

 

This is the global constructor of the myObj2 object that we saw in the preceding source code. The __CTOR_LIST__ variable stores the list of global constructors for an executable or shared library. We find all the global constructor lists by using the info variables command in GDB:

这是我们在前面的源代码中看到的 myObj2 对象的全局构造函数。__CTOR_LIST__ 变量存储可执行文件或共享库的全局构造函数的列表。通过使用 GDB 中的 "info variables" 命令, 我们可以找到所有全局构造函数列表:

(gdb) info variables __CTOR_LIST__

All variables matching regular expression "__CTOR_LIST__":

 

Non-debugging symbols:

0x0804966c __CTOR_LIST__

0x400c0554 __CTOR_LIST__

0x400f95a8 __CTOR_LIST__

0x40101d4c __CTOR_LIST__

0x4021ac7c __CTOR_LIST__

 

We can also find out which libraries these constructor lists belong to (the constructor list at 0x0804966c is the one for the process itself). The list of libraries is as follows:

我们还可以找出这些构造函数列表所属的库 (0x0804966c 中的构造函数列表是进程本身的)。库列表如下所示:

Code View: Scroll / Show All

(gdb) info proc

process 18953

cmdline = '/home/wilding/src/Linuxbook/cpp'

cwd = '/home/wilding/src/Linuxbook'

exe = '/home/wilding/src/Linuxbook/cpp'

 (gdb) shell cat /proc/18953/maps

08048000-08049000  r-xp 00000000  08:13  3649928     /home/wilding/src/Linuxbook/cpp

08049000-0804a000  rw-p 00000000  08:13  3649928     /home/wilding/src/Linuxbook/cpp

40000000-40012000 r-xp 00000000 08:13 1144740 /lib/ld-2.2.5.so

40012000-40013000 rw-p 00011000 08:13 1144740 /lib/ld-2.2.5.so

40013000-40014000 rw-p 00000000 00:00 0

40014000-400ad000 r-xp 00000000 08:13 1847971 /usr/lib/libstdc++.so.5.0.0

400ad000-400c2000 rw-p 00098000 08:13 1847971 /usr/lib/libstdc++.so.5.0.0

400c2000-400c7000 rw-p 00000000 00:00 0

400d7000-400f9000 r-xp 00000000 08:13 1144751 /lib/libm.so.6

400f9000-400fa000 rw-p 00021000 08:13 1144751 /lib/libm.so.6

400fa000-40101000 r-xp 00000000 08:13 1144783 /lib/libgcc_s.so.1

40101000-40102000 rw-p 00007000 08:13 1144783 /lib/libgcc_s.so.1

40102000-40216000 r-xp 00000000 08:13 1144746 /lib/libc.so.6

40216000-4021c000 rw-p 00113000 08:13 1144746 /lib/libc.so.6

4021c000-40221000 rw-p 00000000 00:00 0

bfffe000-c0000000 rwxp fffff000 00:00 0

 

From the output of the /proc file “maps,” we can see all of the address ranges for each shared library. Comparing the addresses of the various__CTOR_LIST__ variables and the address of the libraries, we can find which library contains which constructor list. For example, the constructor list at 0x400c0554 is contained in the second memory segment for the library /usr/lib/libstdc++.so.5.0.0.

从/proc文件 "maps" 的输出中, 我们可以看到每个共享库的所有地址范围。比较 不同的__CTOR_LIST__ 变量的地址和库的地址, 我们可以找到哪个库包含哪个构造函数列表。例如, 0x400c0554 中的构造函数列表包含在库/usr/lib/libstdc ++.so.5.0.0 的第二个内存段中.

Of course, we can do something similar for global destructors, except we need to use the __DTOR_LIST__ variable. Now that we know the symbol names for the constructor and destructor lists, we can reference them directly and do not have to go through _init (or the corresponding _fini) to find the lists.

当然, 我们可以为全局析构函数做类似的事情, 除非我们需要使用 __DTOR_LIST__ 变量。既然我们知道构造函数和析构函数列表的符号名称, 我们可以直接引用它们, 并且不必通过 _init (或相应的 _fini) 来查找列表。

6.10.2. Inline Functions

Inline functions are special functions whose instructions can actually be embedded in the function that called the inline function. The keyword “inline” is a suggestion to the compiler, not an instruction, so a function may or may not be “inlined.” A compiler may also choose to inline functions that were not declared as inline (for example, a small static function).

内联函数是特殊的函数, 其指令实际上可以嵌入到调用内联函数的函数中。关键字 "inline" 是对编译器的一个建议, 而不是指令, 因此函数可能或可能不被 "inline"。编译器还可以把未声明为inline的函数当作内联函数使用 (例如, 小的静态函数)。

An inline function is harder to debug because it will not have a symbol or address (since the code is part of the function that called the inline function). You can’t set a breakpoint by asking GDB to break when entering the inline function (break function1) because an inline function doesn’t really exist as a function. There are also no variables that you can print out—and thus no type information, and so on.

内嵌函数很难调试, 因为它不会有符号或地址 (因为代码是调用内嵌函数的函数的一部分)。在输入内联函数 (break function1) 时, 不能通过要求 GDB 设置断点, 因为内联函数实际上并不作为函数存在。也没有可以打印出来的变量, 因此没有类型信息, 等等。

If you compile without optimization, the functions will probably not be inlined. Keep in mind that the inline key word is only a suggestion to the compiler. Consider the following simple inline function:

如果在不优化的情况下编译, 则可能不会将这些函数内联。请记住, 内联关键字只是对编译器的建议。请考虑以下简单的内联函数:

inline int foo( int a )

{

 

  int b = 0 ;

 

  for ( b = a ; b < 100 ; b ++ ) ;

 

  noValueGlobInt = a ;

  return b + a ;

}

 

Compiling a program that contains this function without optimization will show that there is a symbol for this function (foo) which means that it has not been inlined by the compiler.

编译包含此函数而不进行优化的程序将显示此函数 (foo) 的符号, 这意味着它没有被编译器内联。

penguin> g++ cpp.C -g -o cpp

penguin> nm cpp | egrep foo

08048514 W _Z3fooi

 

Compiling with optimization (-O3), we’ll see that the function foo no longer has a symbol, which means that the compiler inlined the actual machine code into the calling function.

使用优化编译 (-O3), 我们将看到不再有foo符号, 这意味着编译器将实际的机器代码内联到调用函数中。

penguin> g++ cpp.C -g -o cpp -O3

penguin> nm cpp | egrep foo

penguin>

 

(gdb) info functions foo

All functions matching regular expression "foo":

(gdb) quit

 

If you need to debug an inline function (and assuming you have the source code), rebuild the program with -g and without optimization so that you get a static function instead of the inlined function. This works in most cases except when the problem disappears when the program is rebuilt.

如果需要调试内联函数 (假设您有源代码), 请使用-g 和不优化来重新生成程序, 以便获得静态函数而不是内置函数。这在大多数情况下都有效, 除非在重建程序时问题消失。

6.10.3. Exceptions

C++ exceptions are a special way of handling unexpected conditions. The reasons and methods of using exceptions won’t be covered here, but we will cover how they relate to GDB. GDB has commands to handle exceptions, but they may not be well supported. For example:

c++ 异常是处理意外情况的一种特殊方法。这里讲述使用异常的原因和方法, 但我们将讲述它们与 GDB 的关系。GDB 有处理异常的命令, 但可能支持得不太好。例如:

(gdb) catch throw

warning: Unsupported with this platform/compiler combination.

warning: Perhaps you can achieve the effect you want by setting

warning: a breakpoint on __raise_exception().

 

Okay, let’s try that:

(gdb) break __raise_exception

Function "__raise_exception" not defined.

 

We can’t set a breakpoint in__raise_exception because it doesn’t exist. So we’re on our own and have to find another method to catch exceptions in GDB. Let’s take a closer look at what happens when an exception is thrown using the following simple program:

我们不能在__raise_exception中设置断点, 因为它不存在。因此, 我们必须依靠自己找到另一种方法来捕获 GDB 中的异常。让我们进一步了解在运行以下简单程序引发异常时将发生的情况:

Code View: Scroll / Show All

#include <unistd.h>

 

#include <iostream>

#include <typeinfo>

using namespace std;

 

int main()

{

  getpid( ) ; // to mark where we entered the main function

 

  try

  {

    throw ( 5 ) ;

  }

  catch( int )

  {

    cout << "Exception raised.";

  }

 

  try

  {

    throw ( 5 ) ;

  }

  catch( int )

  {

    cout << "Second exception raised.";

  }

 

  return 0;

}

 

This simple program throws two identical exceptions. The sole purpose of the first exception is to flush out the dynamic linking required to find the functions used by the exception handling. The curious reader can try the exercise that follows on the first exception to see how much is involved in finding the function symbols behind the scenes (for more information on this, see Chapter 9, “ELF: Executable and Linking Format”).

此简单程序引发两个相同的异常。第一个异常的唯一目的是刷新查找异常处理所使用的函数所需的动态链接。好奇的读者可以尝试在第一个异常之后进行练习, 以查看在幕后查找函数符号所涉及的内容 (有关这方面的更多信息, 请参见9章 "ELF: 可执行文件和链接格式")。

The next step is to compile this program with -g and create an executable:

下一步是使用-g 编译此程序并创建可执行文件:

penguin> g++ excp.C -o excp -g

 

Then we need to start GDB and run the program, excp, from within GDB (steps are not included here). Next, we set a breakpoint in main and run the program as follows:

然后, 我们需要启动 GDB 和运行程序,excp, 从 GDB 内 (步骤没有包括在这里)。接下来, 我们在 main 中设置一个断点, 然后运行该程序, 如下所示:

(gdb) break main

Breakpoint 1 at 0x804877d: file excp.C, line 9.

(gdb) run

Starting program: /home/wilding/src/Linuxbook/excp

 

Breakpoint 1, main () at excp.C:9

9      getpid( ) ; // to mark where we entered the main function

 

As just discussed, we’ll go past the first exception using the GDB command next until we’re on the second exception.

正如刚才所讨论的, 我们将在第二个异常之前使用 GDB 命令来越过第一个异常。

(gdb) next

13       throw ( 5 ) ;

(gdb) next

15     catch( int )

(gdb) next

17       cout << "Exception raised.";

(gdb) next

22       throw ( 5 ) ;

 

So, according to GDB, we’re about to throw the second exception. We need to switch to using stepi to see what happens under the covers:

因此, 根据 GDB, 我们将要抛出第二个异常。我们需要改用 stepi 来查看下面的内容:

(gdb) stepi

0x08048807   22       throw ( 5 ) ;

(gdb) stepi

0x08048809   22       throw ( 5 ) ;

(gdb) stepi

0x080485e4 in __cxa_allocate_exception ()

 

So according to GDB, the function __cxa_allocate_exception is called when an exception is thrown (at least on this Linux system—other systems might be slightly different). We could set a breakpoint on this function to catch exceptions in GDB. Before we do that, let’s see what else is involved in an exception being thrown (a few stepi calls are omitted for simplicity):

因此, 根据 GDB, 在抛出异常时调用函数 __cxa_allocate_exception (至少在这个 Linux 系统上, 其他系统可能稍有不同)。我们可以在这个函数上设置断点以捕获 GDB 中的异常。在我们这样做之前, 让我们看看在引发的异常中还涉及了什么 (简单地省略了几个 stepi 调用):

Code View: Scroll / Show All

(gdb) stepi

0x4009e478 in operator delete[](void*, std::nothrow_t const&) () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4009e47b in operator delete[](void*, std::nothrow_t const&) () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4009e494 in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4009e49a in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4009e49d in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4009e4a0 in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4009e4a3 in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4004cea0 in _init () from /usr/lib/libstdc++.so.5

(gdb) stepi

0x4016fd70 in malloc () from /lib/libc.so.6

 

There are two things worth noting from this output: 在这个输出中,有两件事值得注意:

  1. Exception handling apparently calls the delete operator.
  2. Exception handling apparently calls malloc.

This is important to know because it means that exceptions should not be used for out of memory conditions or for memory corruptions in the process heap. For lack of memory, throwing an exception could fail because malloc would fail to allocate memory. For memory corruption, there is a chance that malloc or delete would trap because memory corruption in the process heap often affects the data structures used by functions like malloc. When malloc attempts to read its data structures, the memory corruption could force it to use an incorrect pointer causing the trap. In any case, the key point here is to be careful when using exceptions that are due to a trap or for out of memory conditions.

这一点很重要, 因为它意味着不应在内存不足或进程堆中的内存损坏时使用异常。由于内存不足, 抛出异常可能会失败, 因为 malloc 无法分配内存。对于内存损坏, 由于进程堆中的内存损坏通常会影响malloc 这样的函数所使用的数据结构, 因此可能会出现 malloc 或 delete。当 malloc 试图读取其数据结构时, 内存损坏可能会迫使它使用错误的指针导致陷阱。在任何情况下, 这里的关键点是在由于陷阱或内存不足条件时,使用异常时要小心。

Going back to the function __cxa_allocate_exception, we can indeed use this to catch an exception in GDB:

回到函数 __cxa_allocate_exception, 我们确实可以使用它来捕获 GDB 中的一个异常:

(gdb) break __cxa_allocate_exception

Breakpoint 1 at 0x80485e4

(gdb) run

Starting program: /home/wilding/src/Linuxbook/excp

Breakpoint 1 at 0x4009e486

 

Breakpoint 1, 0x4009e486 in __cxa_allocate_exception () from /usr/lib/

libstdc++.so.5

(gdb) where

#0 0x4009e486 in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5

#1  0x0804878c in main  () at excp.C:13

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

 

From the output here, the program caused the breakpoint on the call to __cxa_allocate_exception, which is called from line 13 of the source file excp.C. By moving up one stack frame, we can see the original line of code that triggered the exception:

从这里的输出中, 程序在调用 __cxa_allocate_exception 时导致断点, 从源文件 excp.c 的第13行调用。通过移动一个栈帧, 我们可以看到触发异常的原始代码行:

(gdb) up

#1 0x0804878c in main () at excp.C:13

13       throw ( 5 ) ;

6.11. Threads

Threads are light weight processes that run in the same address space. They share the process heap, the shared libraries and the executable code. The only thing that threads do not share is their individual stacks. Technically, each thread has read/write access to the other thread stacks but never purposefully reads or writes to any stack but its own.

线程是在同一地址空间中运行的轻量进程。它们共享进程堆、共享库和可执行代码。线程不共享的唯一东西是它们各自的栈。从技术上讲, 每个线程都具有对其他线程栈的读/写访问权限, 但从不有意地读取或写入其它栈, 只读写它自己的栈。

Problems in threaded applications can be very challenging to diagnose. When using threads (or processes that use shared memory), there is the potential for race conditions, also known as timing conditions. A race condition is a problem that may or may not occur depending on the order and/or duration of events between threads. For example, thread #1 could read the size of a list from memory and then start to use the list based on the size. While thread #1 is reading through the list, thread #2 could change the list or the size of the list, leading to inconsistent information for thread #1. If thread #1 reads through the list before thread #2 changes it, there would be no problem. Likewise, if thread #1 reads quickly (before thread #2 gets a chance to actually make changes), there would be no problem. Race conditions are often difficult to reproduce because changes that affect timing (such as attaching with GDB) can change the order or duration of events, effectively masking the problem.

线程应用程序中的问题对诊断非常有挑战性。当使用线程 (或使用共享内存的进程) 时, 可能存在race condition, 也称为timing condition。race condition是一个问题可能会发生, 也可能不会出现, 具体取决于线程之间事件的顺序和/或持续时间。例如, 线程 #1 可以从内存中读取列表的大小, 然后根据大小开始使用列表。当线程 #1 正在读取列表时, 线程 #2 可能更改列表或列表的大小, 从而导致线程 #1 的信息不一致。如果线程 #1 在线程 #2 更改之前读取列表, 则不会出现问题。同样, 如果线程 #1 快速读取 (在线程 #2 获得实际更改的机会之前), 就没有问题了。由于timing (如附加到 GDB) 更改的影响可能会更改事件的顺序或持续时间, 从而有效地掩盖了问题, 因此race condition通常很难重现。

Another challenging problem type when using threads is a multi-threaded memory corruption. In this case, one thread corrupts a range of memory, and another thread reads the memory (and the corrupted data) at some point in the future. This is a challenging type of problem since the symptom of the problem can be very disconnected from the original cause (and it can be difficult to tie the two together).

使用线程时另一个具有挑战性的问题类型是多线程内存损坏。在这种情况下, 一个线程会损坏内存, 而另一个线程在将来某个时刻读取内存 (和损坏的数据)。这是一个具有挑战性的问题, 因为问题的症状与根本原因没有联系 (并且可能很难将两者结合在一起)。

Watchpoints can really help out with multi-threaded memory corruptions, but unfortunately, watchpoints are generally not well supported with threads in GDB (at least not on Linux). The main problem is that GDB can only watch an address in a single thread. In other words, if you set a watchpoint for writes to an address from thread #1, it will not be triggered if thread #2 writes to the watched address. This really limits the usefulness of watchpoints for threaded applications!

观察点可以帮助解决多线程内存损坏问题, 但不幸的是, 观察点在 GDB (至少不在 Linux 上) 的线程中通常支持得不好。主要的问题是 GDB 只能在单个线程中观察地址。换言之, 如果您将 watchpoint 从线程 #1 中设置为写入地址, 则如果线程 #2 写入所监视的地址, 则不会触发它。这真的限制了线程应用程序应用watchpoint!

If the program or application has some type of trace facility, it may be worth while to check or dump the address range that gets corrupted (if you know it) regularly or for every trace point. This way, you can narrow down the area of code that is corrupting the memory to the functions that were captured in the trace around the time that the corruption occurred. Unfortunately, because of the lack of watchpoint support for multi-threaded processes, GDB might not be the best tool for debugging a multi-threaded corruption. In any case, let’s explore what GDB can do with multi-threaded processes.

如果程序或应用程序具有某种类型的跟踪功能, 则可能值得有规律的同时检查或转储被损坏的内存地址 (如果您知道的话)。这样, 您可以缩小内存损坏的代码区域, 使其在bug发生时在跟踪中捕获到函数。不幸的是, 由于缺少对多线程进程的 watchpoint 支持, GDB 可能不是调试多线程内存损坏的最佳工具。在这种情况下, 让我们来探讨 GDB 如何处理多线程进程。

The following program is used to illustrate the basic thread support in GDB. The program creates one thread that uses up stack space up to a certain amount or until the thread runs out of stack space:

下面的程序用于说明 GDB 中的基本线程支持。该程序创建一个线程, 该线程在一定程度上使用栈空间, 或者直到线程耗尽栈空间:

Code View: Scroll / Show All

#include <unistd.h>

#include <stdio.h>

#include <pthread.h>

#include <stdlib.h>

#include <sys/types.h>

 

void burnStackSpace( int *depth )

{

  char foo[8192] ;

 

  (*depth)— ;

  if ( *depth > 0 )

  {

   burnStackSpace( depth ) ;

  }

  else

  {

   sprintf( foo, "Hit final depth!\n" ) ; // actually use "foo" to

                       // ensure the compiler keeps it

   puts( foo ) ;

   sleep( 30 ) ;

  }

}

 

extern "C" void *useStackSpace( void *arg )

{

  int stackSpace = *(int *)arg ;

 

  int depth = ( stackSpace/8192 ) / 2 ;

 

  burnStackSpace( &depth ) ;

 

  return NULL ;

}

 

int main(int argc, char *argv[], char *envp[] )

{

 

  int sRC = 0 ;

  pthread_t newThread ;

  int stackSpace = 1008*1024;

  pthread_attr_t attr;

 

  pthread_attr_init(&attr);

  pthread_attr_setstacksize( &attr, 1024*1024 ) ;

 

   sRC = pthread_create( &newThread, &attr, useStackSpace, (void *)&stackSpace ) ;

 

  pthread_join( newThread, NULL ) ;

 

}

 

The function, burnStackSpace, is a recursive function designed to consume stack space to illustrate what out of stack space conditions look like in GDB. The function, useStackSpace, is the first function that the newly created function calls. The main function simple creates a thread and waits for it to complete.

函数 burnStackSpace 是一个递归函数, 用于使用栈空间,来说明 GDB 中的栈空间条件。函数 useStackSpace 是新创建的函数调用的第一个函数。主函数简单创建一个线程并等待它完成。

Here is the compile line we’ll use to compile the program:

下面编译程序:

penguin> g++ threads.C -o threads -lpthread

 

Notice that we need to include -lpthread for the posix thread library. This is required for multi-threaded applications on Linux.

请注意, 我们需要包括-lpthread 的 posix 线程库。这是 Linux 上多线程应用程序所必需的。

Before we examine this multi-threaded program, it is worth while to mention that the thread support in Linux depends on the version of your Linux kernel. If we run this program on an older Linux kernel (pre-version 2.6), there will be three new “processes” according to a ps listing (one original process and two new threads):

在我们检查这个多线程程序之前, 值得一提的是, linux 中的线程支持取决于 linux 内核的版本。如果我们在一个较旧的 Linux 内核上运行这个程序 (前2.6 版), 根据 ps 列表 (一个原始过程和两个新线程), 将有三个新的 "进程":

penguin> ps -fu wilding | egrep threads | egrep -v grep

wilding 19151 19400 0 08:46 pts/72  00:00:00 threads

wilding 19152 19151 0 08:46 pts/72  00:00:00 threads

wilding 19153 19152 0 08:46 pts/72  00:00:00 threads

 

This is due to the original implementation of threads (known as “Linux Threads”), which made each thread look very much like an individual process. It is also worth nothing that there are three threads when we would expect only two—the main thread and the thread created by the main thread. The extra thread may or many not be created, again depending on the implementation of threads on your system.

这是由于线程的原始实现 (称为 "Linux 线程"), 使得每个线程看起来非常像一个单独的进程。值得注意的是, 当我们只希望两个-主线程和主线程创建的线程时,实际却有三个线程时。额外的线程可能会被创建, 这取决于系统中线程的实现。

In a more recent Linux kernel (version 2.6+), you’ll see only one process because the threads will not show up as processes as in earlier Linux kernels:

在最新的 linux 内核 (版本 2.6 +) 中, 您只会看到一个进程, 因为线程不会像以前的 linux 内核那样显示为进程:

penguin2> ps -fu wilding |egrep threads

wilding 22959 22534 0 13:05 pts/10  00:00:00 threads

wilding 22964 22865 0 13:05 pts/16  00:00:00 /bin/grep -E threads

 

This newer thread implementation is called Native POSIX Threading Library (NPTL) and is included in recent versions of the Linux kernel. Despite the differences, the two implementations can be treated pretty much the same way from within GDB. In any case, let’s see what these threads look like from within GDB, using the info threads command:

较新的线程实现称为本机 POSIX 线程库 (NPTL), 并包含在 Linux 内核的最新版本中。尽管存在差异, 但这两种实现在 GDB 内部的处理方式几乎相同。无论如何, 让我们使用 "info thread" 命令来查看 GDB 内部的这些线程是什么样子的:

(gdb) info threads

 2 Thread 1074788704 (LWP 22996) 0x0000002a95c6d2af in __write_nocancel

() from /lib64/tls/libc.so.6

* 1 Thread 182902990144 (LWP 22979) 0x0000002a95673f9f in pthread_join

() from /lib64/tls/libpthread.so.0

 

From the preceding output, we can see two threads: the main thread and the one we created with pthread_create(). The * character beside thread #1 in the output indicates that the current thread in GDB is thread #1.

从前面的输出, 我们可以看到两个线程: 主线程和一个我们用pthread_create ()创建的线程。输出中线程 #1 旁边的 * 字符表示 GDB 中的当前线程是 #1 线程。

Let’s display the stack trace for thread #1 to see which thread it is and what it is doing. We can use the bt (back trace) command in GDB to get the stack trace of the current thread (which is thread #1).

让我们显示线程 #1 的栈跟踪, 以查看它是哪个线程以及它在做什么。我们可以使用 GDB 中的 bt (back trace) 命令获取当前线程的栈跟踪 (线程 #1)。

(gdb) bt

#0 0x0000002a95673f9f in pthread_join () from /lib64/tls/libpthread.so.0

#1  0x00000000004008ec   in  main      (argc=1, argv=0x7fbfffedf8,

envp=0x7fbfffee08) at threads.C:53

 

From the stack trace output, thread #1 is the main thread for the process given that it has the main function on the stack. This would have been the only thread used by the process if we hadn’t created any additional threads.

从栈跟踪输出中, 线程 #1 是进程的主线程, 因为它在栈上具有主函数。如果没有创建任何其他线程, 则此进程将成为该进程使用的唯一线程。

Note: The key word main in used in both main thread and main function is actually a coincidence. The main function is historically the first user function called in a C program and existed long before threads.

注意: 关键字main在主线程和主函数中同时使用实际上是一个巧合。main函数历史上是 C 程序中调用的第一个函数, 并且在线程之前存在了很长时间。

 

Now, let’s switch to the second thread using the thread command in GDB with 2 as the argument:

现在, 让我们使用 GDB 中的thread 2命令切换到第二个线程:

(gdb) thread 2

[Switching to thread 2 (Thread 1074788704 (LWP 22996))]#0

0x0000002a95c6d2af in __write_nocancel ()

  from /lib64/tls/libc.so.6

 

This changed the current thread context used by GDB to that of thread #2. All commands that used to act on thread #1 now act on thread #2. For example, the bt command will now display the stack trace for thread #2:

这将GDB使用的当前线程上下文更改为线程#2的线程上下文。过去在线程#1上执行的所有命令现在都在线程#2上运行。 例如bt命令现在将显示线程#2的栈跟踪:

Code View: Scroll / Show All

(gdb) bt 10

#0 0x0000002a95c6d2af in __write_nocancel () from /lib64/tls/libc.so.6

#1 0x0000002a95c25423 in _IO_new_file_write () from /lib64/tls/libc.so.6

#2 0x0000002a95c25170 in new_do_write () from /lib64/tls/libc.so.6

#3 0x0000002a95c253d5 in _IO_new_do_write () from /lib64/tls/libc.so.6

#4 0x0000002a95c25c64 in _IO_new_file_overflow () from /lib64/tls/libc.so.6

#5 0x0000002a95c275cc in _IO_default_xsputn_internal () from /lib64/tls/libc.so.6

#6 0x0000002a95c25393 in _IO_new_file_xsputn () from /lib64/tls/libc.so.6

#7 0x0000002a95c0045d in vfprintf () from /lib64/tls/libc.so.6

#8 0x0000002a95c08aba in printf () from /lib64/tls/libc.so.6

#9  0x000000000040083e   in burnStackSpace (depth=0x400ff7e0) at threads.C:20

(More stack frames follow...)

(gdb)

 

From the stack output, thread #2 is the additional thread we created (it eventually calls burStackSpace() ).

从栈输出中, 线程 #2 是我们创建的附加线程 (它最终调用 burStackSpace ())。

6.11.1. Running Out of Stack Space

One of the most common types of problems for multi-threaded processes is running out of stack space. This is because the amount of stack space for a typical thread is less than that for the main thread. With a small change to the threaded program source code, we can force the spawned thread to exceed its stack space and trap. Then we’ll use GDB to show the typical symptoms of this somewhat common problem.

多线程进程中最常见的问题之一是栈空间不足。这是因为在通常情况下,分配给线程的栈空间,小于主线程的栈空间。通过对线程程序源代码的小改动, 我们可以强制生成的线程超过其栈空间和陷阱。然后, 我们将使用 GDB 来显示这个常见的问题的典型症状。

The small change is to switch the divide sign / to a multiply sign * for the following line of code:

小改动是把除号改为乘号,如下所示:

int depth = ( stackSpace/8192 ) / 2 ;

 

After the change:

int depth = ( stackSpace/8192 ) * 2 ;

 

This will cause the recursive function burnStackSpace to continue past the depth at which the stack space runs out. Here is what the program does now after this small change:

这将导致递归函数 burnStackSpace 继续运行,直至耗尽栈空间。下面是这个小改动之后的程序所做的事情:

penguin> g++ threads.C -o threads -lpthread -g

penguin> threads

Segmentation fault

 

The first thing to notice is that the common symptom for this type of problem is a segmentation fault (with a signal SIGSEGV). There are additional symptoms from within GDB:

首先要注意的是, 此类问题的常见症状是分段故障 (带有信号 SIGSEGV)。GDB 内部还有其他症状:

(gdb) run

Starting program: /home/wilding/threads

[Thread debugging using libthread_db enabled]

[New Thread 182902990144 (LWP 23102)]

gdb threads

[New Thread 1074788704 (LWP 23123)]

 

Program received signal SIGSEGV, Segmentation fault.

[Switching to Thread 1074788704 (LWP 23123)]

0x000000000040080a in burnStackSpace (depth=0x400ff7e0) at threads.C:15

15      burnStackSpace( depth ) ;

 

According to the output in GDB, the trap occurred on the call to burnStackSpace(), a call to a function. This is a pretty good hint that we’ve run out of stack space considering the segmentation fault did not occur while trying to access a bad pointer or some other typical reason.

根据 GDB 的输出, 陷阱发生在调用 burnStackSpace ()。这是一个很好的提示, 我们已经耗尽栈空间。考虑在试图访问错误指针或其他典型原因时没有出现分段错误。

Further, from the assembly listing of the trapped instruction, we see the following:

此外,从被trap的指令的汇编程序中,我们看到以下内容

Code View: Scroll / Show All

gdb) disass 0x000000000040080a 0x000000000040081a

Dump of assembler code from 0x40080a to 0x40081a:

0x000000000040080a <_Z14burnStackSpacePi+34>:  callq 0x4007e8 <_Z14burnStackSpacePi>

0x000000000040080f <_Z14burnStackSpacePi+39>:  jmp 0x400848 <_Z14burnStackSpacePi+96>

0x0000000000400811 <_Z14burnStackSpacePi+41>:  mov 0xfffffffffffffff8(%rbp),%rax

0x0000000000400815 <_Z14burnStackSpacePi+45>:  lea 0xffffffffffffdff0(%rbp),%rdi

End of assembler dump.

 

The trapped instruction was a callq (a 64-bit call instruction for the x86-64 platform). This is even more conclusive that we’ve run out of stack space. The instruction that caused the segmentation fault is trying to call a function, which in turn is trapping because there is no more space on the stack for the called function.

被trap的指令是 callq (x86-64 平台的64位调用指令)。这是更确凿的结论, 我们已经耗尽了栈空间。导致分段错误的指令试图调用函数, 反过来又被trap, 因为调用函数的栈上没有更多的空间。

There is actually quite a lot to know about using and debugging threads that is unrelated to GDB. If you are planning to do a great deal of multi-threaded programming, it would be worthwhile to buy a book specifically on this subject.

对于使用和调试与线程, 实际上有很多了解。如果你打算做大量的多线程编程, 值得买一本专门讨论这个问题的书。

6.12. Data Display Debugger (DDD)

GDB is a very powerful debugger, but many shy away from it because it is command line-based and has no graphical interface. The Data Display Debugger (DDD) is an X11-based GUI front-end to GDB. It offers many powerful features that can greatly enhance any debugging session. It is available with most distributions—SUSE 9.0 Professional includes it in the package ddd-3.3.7.15; however, SUSE Linux Enterprise Server (SLES) 8 does not. SLES 8 is compatible with SUSE Professional 8.1, so the package ddd-3.3.1-340.x86_64.rpm can be used from SUSE Pro 8.1. Alternatively, you can download the source tarball and build DDD yourself.

GDB 是一个非常强大的调试器, 但许多人回避它, 因为它是基于命令行的, 没有图形界面。数据显示调试器 (DDD) 是基于 X11的 GDB GUI。它提供了许多强大的功能, 可以大大增强调试。在多数linux发行版上都可以找到DDD。SUSE 9.0 专业版在在软件 ddd-3.3.7.15包括它; 但是, SUSE Linux 企业服务器 (SLES) 8 没有。SLES 8 兼容 SUSE 8.1专业版, 所以软件 ddd-3.3. 1-340. rpm 可在 SUSE Pro 8.1 使用。或者, 你可以下载源包自己编译一个DDD。

The following section includes a high-level overview for those who might prefer DDD over GDB.

A great characteristic of DDD is that it only builds on GDB’s functionality; in fact, the DDD GUI includes a GDB console window in which you can type commands directly to GDB just as if you were using it by itself. Figure 6.3 shows a freshly launched DDD window by executing “ddd hello” with the four main windows highlighted and labeled.

下面的部分概述了工作在 GDB 以上的DDD。 DDD 的一个很大的特点是它建立在 GDB 的功能上; 实际上, DDD GUI 包括一个 gdb 控制台窗口, 您可以将命令直接键入到 gdb, 就好像您自己在使用它一样。图6.3 显示了一个新弹出的 DDD 窗口, 执行 "ddd 您好",还有四个带有突出显示和标签的主窗口。

Figure 6.3. Basics of DDD.

[View full size image]

 

The figure shows four main areas of the graphical interface:

图中显示了图形界面的四个主要区域:

  1. Data Display
  2. Source Code
  3. Machine Language
  4. GDB Console

The first three areas of the graphical interface cover the three types of information that a problem investigator will need to see the most. The last area is the actual GDB console and can be used to send a direct command to GDB.

图形界面的前三个区域涵盖了问题调查人员最需要查看的三类信息。最后一个区域是实际的 GDB 控制台, 可用于向 GDB 发送直接命令。

Each of these viewing areas or windows is explained in more detail in the following sections.

以下各节将详细说明每个查看区域或窗口。

6.12.1. The Data Display Window

This window acts as a free-form style workspace where any kind of data that can be printed in a GDB console can be “graphed.” The term “graphed” simply refers to the graphical and dynamic displaying of the selected data. The best feature of the data display window is that after each time the debugger stops, all graphed data in the display window is updated, and each individual change within each graph is highlighted. This makes seeing exactly what changed after each instruction or set of instructions extremely easy. For example, when debugging at the machine language level, it’s very valuable to monitor the machine’s registers to see what changes after each machine instruction. Figure 6.4 shows how this works after executing a single machine language instruction. There are two ways to display the set of registers in the data display window. One way is to execute this command in the GDB console window:

此窗口充当自由格式的工作区, 可以在 GDB 控制台中打印任何类型的数据。术语 "图表" 只是指所选数据的图形化和动态显示。"数据显示" 窗口的最佳功能是: 每次调试器停止后, 显示窗口中的所有图表数据都会被更新, 并且每个图表中的每个更改都会被突出显示。这使得查看每条指令或一组指令之后发生的变化非常容易。例如, 在机器语言级别进行调试时, 监视计算机的寄存器以查看每台机器指令后的变化是非常有价值的。图6.4 显示了执行单条机器语言指令后,该操作的工作原理。在 "数据显示" 窗口中显示寄存器集的方法有两种。一种方法是在 GDB 控制台窗口中执行此命令:

Figure 6.4. Showing registers.

[View full size image]

 

graph display 'info registers'

 

The second way is to click “Data” then “Status Displays.” In the window that pops up, select “List of integer registers and their contents.” Also note that for anything to be displayed in the registers, the program needs to be running, so using the simple “hello” example in Figure 6.3, I set a breakpoint in main and then started the program.

第二种方法是单击 "Data" 然后单击 " Status Displays "。在弹出窗口中, 选择 " List of integer registers and their contents"。 另外请注意, 对于在寄存器中显示的任何内容, 程序都需要运行, 所以在图6.3 中使用简单的 "hello" 示例, 我在 main 中设置一个断点, 然后启动该程序。

As we can see in Figure 6.4, after executing the instruction sub $0xc, %esp (which we can see by stepi in the GDB console window), the “Registers” graph in the data display window has the esp, eip, and eflags highlighted, showing that those registers were modified during this instruction’s execution.

如图6.4 所示, 在执行指令sub $0xc %esp (我们可以在 GDB 控制台窗口中看到 stepi) 后, "Data Display" 窗口中的 "Registers" 图形具有 esp、eip 和 eflags 突出显示, 表明这些寄存器被此指令修改了。

6.12.1.1. Viewing the Raw Stack

Examining the raw stack can be very useful for diagnosing problems, especially when debugging applications not compiled with debug symbols. DDD does not provide a simple menu option to do this, however. Using the data display window and with a little knowledge of stacks in general (see Chapter 5 for more information), we can get the information we need. We know that the esp register on the 32bit x86 architecture (rsp on the 64-bit x86-64 architecture) always points to the top of the stack (smallest memory address), so we can graph a display of the top 16 words on the stack at any given time by entering this command into the GDB console window:

graph display 'x /16wx $esp'

检查原始栈对于诊断问题非常有用, 尤其是在调试未使用调试符号编译的应用程序时。但是, DDD 没有提供简单的菜单选项来检查原始栈。使用 "Data Display" 窗口和一般的栈知识 (参见第5章了解更多信息), 我们可以得到我们需要的信息。我们知道, 在 32位 x86 体系结构 (64 位 x86-64 体系结构是rsp) 上的 esp 寄存器总是指向堆栈的顶部 (最小的内存地址), 因此, 我们可以在任何给定的时间将栈上16个字的图形化显示, 通过输入此命令到 GDB 控制台窗口: graph display "x/16wx $esp"

Note: Using this command will work fine, but the graph that gets created in the display window will only have a title of “X”. This is because DDD simply uses the first word of the expression for the title. Remove the spaces in the expression to make the graphs unique and a little more meaningful, especially when dealing with many graphs. For example, for figure 6.5 this command was used:

     graph display 'x/16wx$esp'


注意: 此命令将工作正常, 但在显示窗口中创建的图表将只具有 "X" 的标题。这是因为 DDD 简单地使用了表达式的第一个词作为标题。移除表达式中的空格, 使图表具有唯一性和更有意义, 尤其是处理许多图形时。例如, 对于图6.5 使用了此命令: graph display "x/16 wx $ esp"

Figure 6.5. Raw stack trace.

[View full size image]

 

 

Figure 6.5 shows a DDD session of a hello.c program, which has code to assign a value to a local variable. The source code is shown in the source window. The green arrow points to the line of source code that will be executed next, so we can see that we have just executed the code that stores the value 5 into our stack_int variable. The graph highlights the line that changed, thus showing us our stack_int variable getting updated directly in memory (note the 0x00000005 value).

图6.5 显示了一个 hello.c 程序的 DDD 会话, 它有将值赋给局部变量的代码。源代码显示在源窗口中。绿色箭头指向即将执行的下一源代码行, 因此我们可以看到, 我们刚刚执行了将值5存储到我们的 stack_int 变量中的代码。突出显示的行显示了我们的 stack_int 变量在内存中直接更新 (注意0x00000005 值)。

6.12.1.2. View Complex Data Structures

The data display window is also very powerful for displaying and organizing complex data structures. Linked lists are a fundamental computer science data structure, but when a particular implementation gets quite involved, debugging them can be difficult. Figure 6.6 shows a DDD session using a very simplified linked list implementation added into our hello.c source. The source code is as follows:

数据显示窗口也非常强大, 用于显示和组织复杂数据结构。链表是计算机科学的基本数据结构, 但是当特定的实现用到链表时, 调试它们可能会很困难。图6.6 显示了一个 DDD 会话, 使用一个非常简化的链表实现添加到我们的hello.c 中。源代码如下所示:

Code View: Scroll / Show All

#include <stdio.h>

 

struct linked_list_struct

{

   int list_no;

   int data;

   struct linked_list_struct *pNext;

};

 

int main( void )

{

  int stack_int = 3;

  char stack_string[16];

  struct linked_list_struct *node1 = (struct

     linked_list_struct*)malloc( sizeof(

        struct linked_list_struct ) );

  struct linked_list_struct *node2 = (struct

     linked_list_struct*)malloc( sizeof(

        struct linked_list_struct ) );

  struct linked_list_struct *node3 = (struct

     linked_list_struct*)malloc( sizeof(

        struct linked_list_struct ) );

 

  node1->list_no = 1;

  node1->data = 9234;

  node1->pNext = node2;

 

  node2->list_no = 2;

  node2->data = 2342;

  node2->pNext = node3;

 

  node3->list_no = 3;

  node3->data = 7987;

  node3->pNext = node1;

 

  printf( "DDD is a wonderful GUI frontend to GDB!\n" );

 

  stack_int = 5;

 

  return( 0 );

 

}

Figure 6.6. Linked list.

[View full size image]

 

 

Typically, this isn’t how linked lists would be implemented, but it serves the purpose at hand, which is to demonstrate the capabilities of the data display window. The steps followed to get the DDD session to the stage shown in Figure 6.6 were:

这不是链表的通常用法, 但它服务于手边的目的, 即演示数据显示窗口的功能。将 DDD 会话带到图6.6 所示阶段的步骤如下:

1.

Set a breakpoint after the three nodes get fully set up:

a. Set the breakpoint by double-clicking in the margin just to the left of the source code line containing the call to printf.

b. A stop sign icon should appear indicating a breakpoint has been set

2.

Run the program.

3.

Right-click any occurrence of the node1 variable in the source window and choose “Display *node1” (scroll the source window if needed).

4.

Right-click pNext in the newly created node1 data display graph and choose “Display *().”

5.

Right-click pNext in the newly created node2 data display graph and choose “Display *()” (note that it is not labeled as node2).

6.

Right-click pNext in the newly created node3 data display graph and choose “Display *()” (note that it is not labeled as node3).

As you can see, DDD gives a graphical representation of our simple circular linked list.

如您所见,DDD给出了我们简单的循环链表的图形表示

6.12.2. Source Code Window

As has been shown in the figures from the Data Display Windows section, the source code window can be very critical in a debugging session. It is important to understand however, that it is only available and usable when the program being debugged has been compiled with debug symbols (using the -g compile switch). It’s also important to understand how compiling with optimization (GCC’s -O1, -O2, -O3, -Os options) affects the source code window (see the “Compiling with Optimization” section for more information).

正如在 " Data Display " 窗口显示的的数字, 源代码窗口在调试会话中可能非常关键。不过, 重要的是要了解, 只有当正在调试的程序使用调试符号 (使用 -g 编译开关) 编译时, 才可用。同样重要的是要了解如何使用优化编译 (GCC 的 -O1,-O2, -O3, -Os 选项) 影响源代码窗口 (请参阅 "使用优化编译" 一节以了解更多信息)。

Because compiler optimization often reorganizes the machine level instructions, the order in which the source code level instructions are executed changes. If the program being debugged has been compiled with debug symbols and some level of optimization, stepping through machine level instructions with the stepi or nexti GDB commands will cause the current source line of code marker to appear to jump around wildly from line of code to line of code. This is completely normal and in fact is very useful to debugging compiler optimization problems. It’s quite interesting to see how much work a compiler’s optimizer does in converting what the author might think is optimized code into optimized assembly.

因为编译器优化通常会重组机器级指令, 所以执行源代码级指令的顺序会发生变化。如果正在调试的程序已使用调试符号和某种级别的优化进行编译, 则通过 stepi 或 nexti 等GDB 命令单步执行机器级指令时, 将导致代码标记在源代码行不停跳动。这是完全正常的, 实际上对调试编译器优化问题是非常有用的。在将作者认为是优化的代码转换为优化的汇编程序时, 看到编译器的做了多少优化工作, 这很有趣。

The source code window is also interactive in that breakpoints can be set by double-clicking to the left of any source code line. You can also hover the mouse pointer over any variable to see a pop-up showing its value. Right-clicking the variable presents a menu you can use to print variable values to the GDB console window or display them to the data display window. This is especially useful for structures.

源代码窗口也是交互式的, 可以通过双击任意源代码行的左侧来设置断点。也可以将鼠标指针悬停在任何变量上, 通过弹出窗口查看变量的值。右键单击该变量将显示一个菜单, 您可以使用它将变量值打印到 GDB 控制台窗口中, 或将其显示在 "Data Display" 窗口中。这对于结构尤其有用。

6.12.3. Machine Language Window

The machine language window basically displays the output of GDB’s disassemble command with a specified range within the current function.

"机器语言" 窗口主要显示 GDB 的汇编命令, 并在当前函数中具有指定的范围。

Note: Machine language dumps of functions can be quite lengthy. By default DDD will only disassemble a maximum of 256 bytes at a time in the machine language window. Once execution goes beyond what’s currently disassembled, DDD will disassemble and display another 256 bytes. This can be inconvenient in some cases, so to change this behavior, add the following to your $HOME/.ddd/init file:

    Ddd*maxDisassemble: 512

注: 机器语言的功能转储可能相当长。默认情况下, DDD 将只在 "机器语言" 窗口中一次最多反汇编256个字节。一旦超出, DDD 将反汇编和显示另一个256字节。在某些情况下, 这可能会很不方便, 因此要修改此行, 请将以下内容添加到 $HOME/. ddd/init 文件中: DDD * maxDisassemble: 512

 

Try substituting 512 with whatever value you want; 0 means disassemble the entire function. Be sure to add this line to the top of the init file only, as DDD overwrites some settings with defaults when DDD is terminated.

试着用您想要的任何值替换 512;0表示反汇编整个函数。请确保将此行添加到 init 文件的顶部, 因为在 DDD 退出时, DDD 会覆盖一些默认设置。

The machine language window is also interactive in the same ways that the source code window is. You can set breakpoints on specific instructions as well as print or display memory addresses or symbols. The machine language window is extremely useful even for the simple fact that the next instruction to be executed is pointed to by an arrow. GDB’s text-based interface does not do this, and disassembling the machine language instructions around the current area of execution can be very cumbersome.

"机器语言" 窗口也以与源代码窗口相同的方式进行交互。您可以在特定的指令上设置断点以及打印或显示内存地址或符号。机器语言窗口是非常有用的,在机器语言窗口中可以看到箭头指向下一个要执行的指令。基于文本的 GDB 界面就不这样做, 而且反汇编机器语言指令执行时非常繁琐。

6.12.4. GDB Console Window

The GDB console window is essentially what you get when executing GDB by itself. The difference is that it is being managed by DDD, so commands will be caught by DDD and integrated into its GUI. The beauty of the GDB console is that for people used to just using GDB by itself and for those die-hard command- line users, it still has everything that they’re used to. DDD, however, enhances the GDB console and makes the debugging experience much easier and more efficient.

GDB 控制台窗口本质上是您执行 GDB 时所得到的。不同的是, 它是由 DDD 管理, 所以命令将被DDD和它的 GUI捕获。GDB 控制台的动人之处在于, 对于那些习惯使用 GDB 的人来说, 和对于那些死硬的命令行用户来说, 它仍然拥有他们所习惯的一切。然而, DDD 增强了 GDB 控制台, 使调试体验变得更加容易和高效。

6.13. Conclusion

In an open source environment such as Linux, having a freely available debugger like GDB is part of the normal process of investigating. This is not necessarily the case on commercial operating systems because a) the debugger is often not free and b) the source code for the operating system and its tools is not available to the public. Hopefully after reading this chapter, you have a good understanding of what GDB can do to help with your problem determination needs on Linux.

在 Linux 这样的开源环境中, 像 GDB 这样的免费的调试器是正常调查过程的一部分。在商业操作系统上情况不同, 因为 a) 调试器通常不是免费的, b) 操作系统及其工具的源代码不能公开使用。希望看完本章后, 您可以更好地理解 GDB, 并且能够帮助您解决 Linux 问题。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mounter625

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值