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 問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章