Trasování a ladění nativních aplikací v Linuxu: pokročilejší možnosti nabízené GNU Debuggerem

31. 5. 2016
Doba čtení: 24 minut

Sdílet

Ve třetí části seriálu se seznámíme s tím, jaké možnosti vývojářům nabízí GNU Debugger v případě, že je zapotřebí ladit aplikaci až na úrovni jednotlivých strojových instrukcí.

Obsah

1. Trasování a ladění nativních aplikací v Linuxu: pokročilejší možnosti nabízené GNU Debuggerem

2. Ladění aplikací naprogramovaných v assembleru

3. Překlad aplikace v assembleru se zachováním ladicích informací

4. Tabulka symbolů

5. Načtení aplikace naprogramované v assembleru do GNU Debuggeru

6. Disassembler v GNU Debuggeru

7. Přepnutí režimu zobrazení instrukcí: syntaxe AT&T versus Intel

8. Nastavení breakpointů a krokování po instrukcích

9. Zobrazení obsahu pracovních registrů

10. Zobrazení obsahu vybrané části operační paměti

11. Formátování zobrazovaných dat

12. Užitečný příkaz display

13. Praktický příklad a použití TUI

14. Odkazy na Internetu

1. Trasování a ladění nativních aplikací v Linuxu: pokročilejší možnosti nabízené GNU Debuggerem

V předchozí části seriálu o trasování a ladění nativních aplikací jsme se seznámili se základními možnostmi a příkazy nabízenými GNU Debuggerem. Připomeňme si, že se jednalo například o příkaz list (zkráceně l) určený pro zobrazení zvolené části zdrojového kódu laděné aplikace, dále pak o práci s breakpointy (příkaz break, zkráceně b), krokování po jednotlivých řádcích či se vstupem do volané funkce (příkazy next a step) a v neposlední řadě taktéž o příkaz frame sloužící pro zobrazení vybraných zásobníkových rámců. Již s těmito několika příkazy, pokud se doplní o univerzální příkazy print, run a continue, je možné celkem bez větších problémů odladit i rozsáhlejší aplikaci naprogramovanou v nějakém vyšším programovacím jazyce (typicky v Céčku či C++). Ovšem ve chvíli, kdy nemáme k dispozici zdrojový kód (ladění některých ovladačů umístěných v blobech), chybí ladicí informace, popř. je aplikace naprogramovaná v assembleru, je nutné použít i některé další příkazy, s nimiž se seznámíme v dnešním článku.

2. Ladění aplikací naprogramovaných v assembleru

Aby bylo možné si vyzkoušet všechny dále popisované funkce nabízené GNU Debuggerem v praxi, vytvoříme si velmi jednoduchou aplikaci naprogramovanou v assembleru procesorů řady i386 či x86–64. Používat budeme pouze ty pracovní registry, které jsou kompatibilní s 32bitovou architekturou i386, což ostatně platí i pro volání funkcí Linuxového jádra. Aplikace je velmi jednoduchá a skládá se ze tří částí (ty by bylo možné rozepsat do subrutin či vkládaných maker, ovšem nebudeme aplikaci zbytečně komplikovat). V části první se v počítané programové smyčce vytvoří pole znaků od ‚a‘ do ‚z‘ (nejedná se přitom o klasický řetězec, protože pole nemusí být ukončeno znakem \0, to je jen konvence, kterou zde potřebovat nebudeme). V části druhé je výsledné pole vytištěno na standardní výstup s využitím syscallu sys_write. Část třetí pouze aplikaci korektně ukončí, a to opět s využitím syscallu, tentokrát syscallu nazvaného sys_exit. Vzhledem k tomu, že většina(?) programátorů, co pracují s assemblerem na procesorech řady i386 a x86–64, zná a preferuje Intel syntaxi, je celá aplikace napsána právě s využitím této syntaxe (viz zvýrazněná pseudoinstrukce, která syntaxi přepíná):

# asmsyntax=as
 
# Testovaci program naprogramovany v assembleru GNU as
# - pouzita je "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
 
.intel_syntax noprefix
 
 
# Linux kernel system call table
sys_exit=1
sys_write=4
 
 
 
#-----------------------------------------------------------------------------
.section .data
 
 
 
#-----------------------------------------------------------------------------
.section .bss
        .lcomm buffer, 26            # rezervace bufferu pro vystup
 
 
 
#-----------------------------------------------------------------------------
.section .text
        .global _start               # tento symbol ma byt dostupny i linkeru
 
_start:
        mov   ecx, offset buffer     # zapis se bude provadet do tohoto bufferu
        mov   al, 'a'                # kod prvniho zapisovaneho znaku
loop:
        mov   [ecx], al              # zapis znaku do bufferu
        inc   al                     # ASCII kod dalsiho znaku
        inc   ecx                    # uprava ukazatele do bufferu
        cmp   al, 'z'                # ma se smycka ukoncit?
        jna   loop                   # pokud jsme neprekrocili kod 'z', opakovat smycku
 
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, 1                 # standardni vystup
        mov   ecx, offset buffer     # adresa retezce, ktery se ma vytisknout
        mov   edx, 26                # pocet znaku, ktere se maji vytisknout
        int   0x80                   # volani Linuxoveho kernelu
 
        mov   eax, sys_exit          # cislo sycallu pro funkci "exit"
        mov   ebx, 0                 # exit code = 0
        int   0x80                   # volani Linuxoveho kernelu

3. Překlad aplikace v assembleru se zachováním ladicích informací

Proces vytvoření spustitelného binárního souboru je rozdělen do dvou kroků. V kroku prvním se použije nástroj zvaný assembler (konkrétně GNU Assembler) pro vygenerování objektového kódu. Povšimněte si použití přepínače –gstabs+, který zajistí, že se do objektového kódu vloží i všechny symboly potřebné pro ladění a tudíž i pro GNU Debugger:

as --gstabs+ test.s -o test.o

Tyto symboly sice (mnohdy dosti podstatným způsobem) zvětšují velikost výsledného spustitelného souboru, ovšem na samotný výkon aplikace nemají větší vliv (tato část se ani nemusí načítat do operační paměti):

Přeloženo Velikost
bez ladicích symbolů 1071 bajtů
s ladicími symboly 1503 bajtů
po strip 512 bajtů

Pokud budete chtít později všechny ladicí informace odstranit, může se použít příkaz strip nebo alternativně provést nový překlad, tentokrát bez použití zmíněného přepínače. Po překladu následuje druhá fáze – slinkování. Tato fáze je v naší demonstrační aplikaci velmi jednoduchá, neboť není nutné ani spojit více objektových kódů, ani použít knihovnu (voláme totiž přímo funkce jádra):

ld test.o

Nyní si samozřejmě můžete program vyzkoušet:

./a.out
 
abcdefghijklmnopqrstuvwxyz

4. Tabulka symbolů

Pokud vás zajímá, jak vypadá interní struktura vytvořeného spustitelného souboru a jaké (ladicí i jiné) symboly tento soubor obsahuje, můžete použít následující příkaz, který všechny potřebné informace dokáže získat a zobrazit v čitelné podobě. Nejzajímavější je v tomto okamžiku struktura nazvaná SYMBOL TABLE:

objdump -f -d -t -h a.out
 
a.out:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x00000000004000b0
 
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000034  00000000004000b0  00000000004000b0  000000b0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .bss          00000020  00000000006000e8  00000000006000e8  000000e8  2**3
                  ALLOC
  2 .stab         000000d8  0000000000000000  0000000000000000  000000e4  2**2
                  CONTENTS, READONLY, DEBUGGING
  3 .stabstr      0000001b  0000000000000000  0000000000000000  000001bc  2**0
                  CONTENTS, READONLY, DEBUGGING
SYMBOL TABLE:
00000000004000b0 l    d  .text  0000000000000000 .text
00000000006000e8 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .stab  0000000000000000 .stab
0000000000000000 l    d  .stabstr       0000000000000000 .stabstr
0000000000000000 l    df *ABS*  0000000000000000 test.o
0000000000000001 l       *ABS*  0000000000000000 sys_exit
0000000000000004 l       *ABS*  0000000000000000 sys_write
00000000006000e8 l     O .bss   000000000000001a buffer
00000000004000b7 l       .text  0000000000000000 loop
0000000000000000 l    df *ABS*  0000000000000000
00000000004000b0 g       .text  0000000000000000 _start
00000000006000e4 g       .bss   0000000000000000 __bss_start
00000000006000e4 g       .bss   0000000000000000 _edata
0000000000600108 g       .bss   0000000000000000 _end
 
 
 
Disassembly of section .text:
 
00000000004000b0 <_start>:
  4000b0:       b9 e8 00 60 00          mov    $0x6000e8,%ecx
  4000b5:       b0 61                   mov    $0x61,%al
 
00000000004000b7 <loop>;
  4000b7:       67 88 01                mov    %al,(%ecx)
  4000ba:       fe c0                   inc    %al
  4000bc:       ff c1                   inc    %ecx
  4000be:       3c 7a                   cmp    $0x7a,%al
  4000c0:       76 f5                   jbe    4000b7
  4000c2:       b8 04 00 00 00          mov    $0x4,%eax
  4000c7:       bb 01 00 00 00          mov    $0x1,%ebx
  4000cc:       b9 e8 00 60 00          mov    $0x6000e8,%ecx
  4000d1:       ba 1a 00 00 00          mov    $0x1a,%edx
  4000d6:       cd 80                   int    $0x80
  4000d8:       b8 01 00 00 00          mov    $0x1,%eax
  4000dd:       bb 00 00 00 00          mov    $0x0,%ebx
  4000e2:       cd 80                   int    $0x80

Při spuštění stejného příkazu, ovšem nad binárním souborem bez ladicích symbolů (po strip), bude výpis vypadat zcela jinak:

objdump -f -d -t -h a.out
 
a.out:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00000000004000b0
 
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000034  00000000004000b0  00000000004000b0  000000b0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .bss          00000020  00000000006000e8  00000000006000e8  000000e8  2**3
                  ALLOC
SYMBOL TABLE:
no symbols
 
 
 
Disassembly of section .text:
 
00000000004000b0 <.text>:
  4000b0:       b9 e8 00 60 00          mov    $0x6000e8,%ecx
  4000b5:       b0 61                   mov    $0x61,%al
  4000b7:       67 88 01                mov    %al,(%ecx)
  4000ba:       fe c0                   inc    %al
  4000bc:       ff c1                   inc    %ecx
  4000be:       3c 7a                   cmp    $0x7a,%al
  4000c0:       76 f5                   jbe    0x4000b7
  4000c2:       b8 04 00 00 00          mov    $0x4,%eax
  4000c7:       bb 01 00 00 00          mov    $0x1,%ebx
  4000cc:       b9 e8 00 60 00          mov    $0x6000e8,%ecx
  4000d1:       ba 1a 00 00 00          mov    $0x1a,%edx
  4000d6:       cd 80                   int    $0x80
  4000d8:       b8 01 00 00 00          mov    $0x1,%eax
  4000dd:       bb 00 00 00 00          mov    $0x0,%ebx
  4000e2:       cd 80                   int    $0x80

Alternativně si můžete zobrazit všechny řetězce, mezi nimiž lze nalézt jak jména jednotlivých sekcí (.text, .bss), tak i návěští (_start, loop) i pojmenované symboly (sys_write, sys_exit):

strings a.out
 
test.s
/home/tester/temp/
.symtab
.strtab
.shstrtab
.text
.bss
.stab
.stabstr
test.o
sys_exit
sys_write
buffer
loop
_start
__bss_start
_edata
_end

5. Načtení aplikace naprogramované v assembleru do GNU Debuggeru

Následně se můžeme pokusit o jeho analýzu, krokování a ladění naší testovací aplikace přímo v prostředí GNU Debuggeru, popř. v jeho TUI (textovém uživatelském rozhraní). GNU Debugger spustíme obvyklým způsobem, který již známe z předchozí části tohoto seriálu:

gdb a.out

Důležité je, aby se na posledním řádku úvodní zprávy GNU Debuggeru vypsala informace o tom, že se úspěšně načetly všechny ladicí informace:

GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...done.

Pokud se pouze zobrazí následující zprávy:

GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...(no debugging symbols found)...done.

… znamená to, že assembler do objektového kódu nepřidal ladicí symboly, došlo k jejich odstranění při linkování, nebo byl použit příkaz strip, který tyto symboly odstranil kdykoli později. Samozřejmě je možné ladit a krokovat i program bez přidaných ladicích symbolů (někdy je to dokonce nutné), celý postup je však nepatrně složitější, protože se například ztratí informace o všech návěštích (labels), jménech volaných subrutin apod.

6. Disassembler v GNU Debuggeru

Při práci na úrovni jednotlivých instrukcí velmi často využijeme příkaz disassemble, který lze zkrátit například na disas (ještě kratší zápis již nelze použít, neboť by jméno příkazu kolidovalo s příkazem disable). Za příkazem disassemble je možné uvést návěští, například:

(gdb) disassemble _start
Dump of assembler code for function _start:
   0x00000000004000b0 <+0>:     mov    $0x6000e8,%ecx
   0x00000000004000b5 <+5>:     mov    $0x61,%al
End of assembler dump.

nebo:

(gdb) disassemble loop
Dump of assembler code for function loop:
   0x00000000004000b7 <+0>:     mov    %al,(%ecx)
   0x00000000004000ba <+3>:     inc    %al
   0x00000000004000bc <+5>:     inc    %ecx
   0x00000000004000be <+7>:     cmp    $0x7a,%al
   0x00000000004000c0 <+9>:     jbe    0x4000b7 <loop>
   0x00000000004000c2 <+11>:    mov    $0x4,%eax
   0x00000000004000c7 <+16>:    mov    $0x1,%ebx
   0x00000000004000cc <+21>:    mov    $0x6000e8,%ecx
   0x00000000004000d1 <+26>:    mov    $0x1a,%edx
   0x00000000004000d6 <+31>:    int    $0x80
   0x00000000004000d8 <+33>:    mov    $0x1,%eax
   0x00000000004000dd <+38>:    mov    $0x0,%ebx
   0x00000000004000e2 <+43>:    int    $0x80

Povšimněte si výchozího režimu výpisu. Na začátku každého řádku je adresa, následuje relativní offset od první adresy (z něj lze získat představu o délce instrukce) a poté následuje symbolický kód: mnemotechnické jméno instrukce a její případné operandy. Mimochodem – ona adresa první instrukce 0×00000000004000b0 byla vypsána (a dokonce dvakrát) ve výstupu programu objdump, takže vidíme, že GNU Debugger skutečně „nekecá“:

...
...
...
start address 0x00000000004000b0
...
...
...
  0 .text         00000034  00000000004000b0  00000000004000b0  000000b0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
...
...
...

Samozřejmě je stále možné použít i příkaz list, který známe z předchozí části:

(gdb) list loop
31
32      _start:
33              mov   ecx, offset buffer     # zapis se bude provadet do tohoto bufferu
34              mov   al, 'a'                # kod prvniho zapisovaneho znaku
35      loop:
36              mov   [ecx], al              # zapis znaku do bufferu
37              inc   al                     # ASCII kod dalsiho znaku
38              inc   ecx                    # uprava ukazatele do bufferu
39              cmp   al, 'z'                # ma se smycka ukoncit?
40              jna   loop                   # pokud jsme neprekrocili kod 'z', opakovat smycku

7. Přepnutí režimu zobrazení instrukcí: syntaxe AT&T versus Intel

Pokud se pečlivěji podíváte na výstup z disassembleru, zjistíte, že operandy instrukcí příliš neodpovídají původnímu zdrojovému kódu:

(gdb) disassemble loop
Dump of assembler code for function loop:
   0x00000000004000b7 <+0>:     mov    %al,(%ecx)
   0x00000000004000ba <+3>:     inc    %al
   0x00000000004000bc <+5>:     inc    %ecx
   0x00000000004000be <+7>:     cmp    $0x7a,%al
   0x00000000004000c0 <+9>:     jbe    0x4000b7 <loop>
   0x00000000004000c2 <+11>:    mov    $0x4,%eax
   0x00000000004000c7 <+16>:    mov    $0x1,%ebx
   0x00000000004000cc <+21>:    mov    $0x6000e8,%ecx
   0x00000000004000d1 <+26>:    mov    $0x1a,%edx
   0x00000000004000d6 <+31>:    int    $0x80
End of assembler dump.

V původním zdrojovém kódu jsme ovšem používali Intel syntaxi:

loop:
        mov   [ecx], al              # zapis znaku do bufferu
        inc   al                     # ASCII kod dalsiho znaku
        inc   ecx                    # uprava ukazatele do bufferu
        cmp   al, 'z'                # ma se smycka ukoncit?
        jna   loop                   # pokud jsme neprekrocili kod 'z', opakovat smycku
 
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, 1                 # standardni vystup
        mov   ecx, offset buffer     # adresa retezce, ktery se ma vytisknout
        mov   edx, 26                # pocet znaku, ktere se maji vytisknout
        int   0x80                   # volani Linuxoveho kernelu

Bylo by tedy dobré stejnou syntaxi používat i v GNU Debuggeru. To je skutečně možné, protože každá moderní varianta GNU Debuggeru nabízí následující volbu pro přepnutí režimu disassembleru:

set disassembly-flavor intel

Nyní již výstup disassembleru vypadá lidštěji:

(gdb) disassemble loop
Dump of assembler code for function loop:
   0x00000000004000b7 <+0>:     mov    BYTE PTR [ecx],al
   0x00000000004000ba <+3>:     inc    al
   0x00000000004000bc <+5>:     inc    ecx
   0x00000000004000be <+7>:     cmp    al,0x7a
   0x00000000004000c0 <+9>:     jbe    0x4000b7 <loop>
   0x00000000004000c2 <+11>:    mov    eax,0x4
   0x00000000004000c7 <+16>:    mov    ebx,0x1
   0x00000000004000cc <+21>:    mov    ecx,0x6000e8
   0x00000000004000d1 <+26>:    mov    edx,0x1a
   0x00000000004000d6 <+31>:    int    0x80
End of assembler dump.

Poznámka: instrukce jbe („jump if below or equal“) je totožná s instrukcí jna („jump if not above“) a disassembler samozřejmě neví, kterou variantu jsme použili v původním zdrojovém kódu.

8. Nastavení breakpointů a krokování po instrukcích

Práce s breakpointy se při ladění aplikací na úrovni strojových instrukcí prakticky vůbec neliší od ladění aplikací naprogramovaných v nějakém vyšším programovacím jazyku. Breakpoint lze nastavit na konkrétní instrukci, návěští (což je pravděpodobně nejpoužívanější způsob), nebo na konkrétní adresu. Následují příklady na nastavení breakpointů:

(gdb) break 1
Breakpoint 2 at 0x4000b0: file test.s, line 1.
 
(gdb) break 10
Note: breakpoint 2 also set at pc 0x4000b0.
Breakpoint 3 at 0x4000b0: file test.s, line 10.
 
(gdb) break loop
Breakpoint 1 at 0x4000b7: file test.s, line 36.

Seznam všech breakpointů se získá příkazem info break:

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004000b7 test.s:36
        breakpoint already hit 4 times
2       breakpoint     keep y   0x00000000004000b0 test.s:1
3       breakpoint     keep y   0x00000000004000b0 test.s:10
4       breakpoint     keep y   0x00000000004000b7 test.s:36

Podívejme se nyní na způsob použití breakpointů. Předpokládejme nastavení jediného breakpointu na návěští loop:

(gdb) clear 1
Deleted breakpoint 5
(gdb) clear 10
Deleted breakpoint 6
(gdb) clear loop
Deleted breakpoint 7
(gdb) break loop
Breakpoint 8 at 0x4000b7: file test.s, line 36.

Po spuštění programu se jeho běh podle očekávání zastaví na breakpointu, o čemž je vývojář samozřejmě informován:

(gdb) run
Starting program: /home/tester/temp/a.out
 
Breakpoint 1, loop () at test.s:36
36              mov   [ecx], al              # zapis znaku do bufferu

Pro krokování na úrovni jednotlivých instrukcí se používá příkaz nexti, který lze zkrátit na ni:

(gdb) nexti
37              inc   al                     # ASCII kod dalsiho znaku
(gdb) ni
38              inc   ecx                    # uprava ukazatele do bufferu
(gdb) ni
39              cmp   al, 'z'                # ma se smycka ukoncit?

Tomuto příkazu je možné zadat i počet provedených instrukcí:

(gdb) nexti 10

Po několika provedeních smyčky (například příkazem c(ontinue)) se můžeme podívat na aktuální stav breakpointů:

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004000b7 test.s:36
        breakpoint already hit 4 times

9. Zobrazení obsahu pracovních registrů

Při ladění a krokování aplikací na úrovni jednotlivých strojových instrukcí se prakticky vždy potřebujeme dozvědět i to, jaké hodnoty jsou uloženy v pracovních registrech. V případě, že chceme znát hodnotu jednoho registru, lze použít univerzální příkaz print, přičemž před jménem registru musí být uveden znak $, aby GNU Debugger věděl, že se neptáme na obsah proměnné:

(gdb) print $al
$1 = 100
 
(gdb) print $ecx
$3 = 6291691

Pokud se má hodnota registru vypsat v jiné soustavě, stačí za příkaz print přidat formátovací znak:

(gdb) print/x $ecx
$5 = 0x6000eb
 
(gdb) print/t $ecx
$7 = 11000000000000011101011
 
(gdb) print/c $al
$10 = 100 'd'

K dispozici jsou tyto formátovací znaky:

o(octal)
x(hex)
d(decimal)
u(unsigned decimal),
t(binary)
f(float)
a(address)
c(char)

Všechny pracovní, stavové a řídicí registry se vypisují příkazem info registers. Na architektuře x86–64 vypadá výsledek následovně:

(gdb) info registers
rax            0x64     100
rbx            0x0      0
rcx            0x6000eb 6291691
rdx            0x0      0
rsi            0x0      0
rdi            0x0      0
rbp            0x0      0x0
rsp            0x7fffffffe190   0x7fffffffe190
r8             0x0      0
r9             0x0      0
r10            0x0      0
r11            0x0      0
r12            0x0      0
r13            0x0      0
r14            0x0      0
r15            0x0      0
rip            0x4000b7 0x4000b7 <loop>
eflags         0x293    [ CF AF SF IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

Registry matematického koprocesoru se vypíšou příkazem info float, přičemž formát výstupu opět závisí na použité architektuře:

(gdb) info float
  R7: Empty   0x00000000000000000000
  R6: Empty   0x00000000000000000000
  R5: Empty   0x00000000000000000000
  R4: Empty   0x00000000000000000000
  R3: Empty   0x00000000000000000000
  R2: Empty   0x00000000000000000000
  R1: Empty   0x00000000000000000000
=>R0: Empty   0x00000000000000000000
 
Status Word:         0x0000
                       TOP: 0
Control Word:        0x037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest
Tag Word:            0xffff
Instruction Pointer: 0x00:0x00000000
Operand Pointer:     0x00:0x00000000
Opcode:              0x0000

10. Zobrazení obsahu vybrané části operační paměti

Nyní již víme, jak lze zobrazit disassemblovaný kód (sekvenci strojových instrukcí) i obsah pracovních a řídicích registrů, včetně registrů matematického koprocesoru. Stav procesu je ovšem navíc charakterizován i obsahem paměti, která je procesu přidělena. Pro výpis obsahu bloku paměti použijte příkaz x, za nímž může následovat adresa. Pokud víme, že buffer začíná na adrese 0×6000e8, lze si jeho začátek zobrazit příkazem:

(gdb) x 0x6000e8
0x6000e8 <buffer>:      97 'a'

Namísto explicitně zapsané adresy lze použít například i obsah registru. My pro adresování používáme registr ecx a již víme, že před jménem registru musí být zapsán znak dolaru:

(gdb) x $ecx
0x6000eb <buffer+3>:    0x00000000

Při požadavku na výpis delšího bloku se za lomítko zadá počet prvků:

(gdb) x/10 $ecx
0x6000eb <buffer+3>:    0x00000000      0x00000000      0x00000000      0x00000000
0x6000fb <buffer+19>:   0x00000000      0x00000000      0x00000000      0x00000000
0x60010b:       0x00000000      0x00000000

Povšimněte si, jak GNU Debugger inteligentně zjistí, které adresy ještě náleží do bufferu a které už nikoli. Ještě zřejmější to bude při pokusu o výpis paměti, na níž jsou uloženy instrukce tvořící vlastní program:

(gdb) x/50bx _start
0x4000b0 <_start>:      0xb9    0xe8    0x00    0x60    0x00    0xb0    0x61    0x67
0x4000b8 <loop+1>:      0x88    0x01    0xfe    0xc0    0xff    0xc1    0x3c    0x7a
0x4000c0 <loop+9>:      0x76    0xf5    0xb8    0x04    0x00    0x00    0x00    0xbb
0x4000c8 <loop+17>:     0x01    0x00    0x00    0x00    0xb9    0xe8    0x00    0x60
0x4000d0 <loop+25>:     0x00    0xba    0x1a    0x00    0x00    0x00    0xcd    0x80
0x4000d8 <loop+33>:     0xb8    0x01    0x00    0x00    0x00    0xbb    0x00    0x00
0x4000e0 <loop+41>:     0x00    0x00

11. Formátování zobrazovaných dat

Příkaz x kromě počtu prvků akceptuje i počet prvků a formátovací znak. Zobrazme si tedy padesát bajtů začínajících na adrese odpovídající začátku bufferu. Každý bajt bude zobrazen v dekadické podobě:

(gdb) x/50b 0x6000e8
0x6000e8 <buffer>:      97      98      99      0       0       0       0       0
0x6000f0 <buffer+8>:    0       0       0       0       0       0       0       0
0x6000f8 <buffer+16>:   0       0       0       0       0       0       0       0
0x600100 <buffer+24>:   0       0       0       0       0       0       0       0
0x600108:       0       0       0       0       0       0       0       0
0x600110:       0       0       0       0       0       0       0       0
0x600118:       0       0

Přepnutí do režimu hexadecimálního výpisu vypadá takto:

(gdb) x/50bx 0x6000e8
0x6000e8 <buffer>:      0x61    0x62    0x63    0x00    0x00    0x00    0x00    0x00
0x6000f0 <buffer+8>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f8 <buffer+16>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600100 <buffer+24>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600108:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600110:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600118:       0x00    0x00

Kombinace předchozího:

(gdb) x/10xb $ecx-3
0x6000e8 <buffer>:      0x61    0x62    0x63    0x00    0x00    0x00    0x00    0x00
0x6000f0 <buffer+8>:    0x00    0x00

Zajímavý může být režim výpisu „řetězců“, kdy se GNU Debugger pokusí část paměti považovat za céčkovský řetězec. To bude fungovat i pro náš buffer, a to z toho důvodu, že se sekce .bss automaticky nuluje při startu procesu a tudíž vlastně zadarmo získáme znaky pro konec řetězce \0:

(gdb) x/10xs $ecx-3
0x6000e8 <buffer>:      "abc"
0x6000ec <buffer+4>:    ""
0x6000ed <buffer+5>:    ""
0x6000ee <buffer+6>:    ""
0x6000ef <buffer+7>:    ""
0x6000f0 <buffer+8>:    ""
0x6000f1 <buffer+9>:    ""
0x6000f2 <buffer+10>:   ""
0x6000f3 <buffer+11>:   ""
0x6000f4 <buffer+12>:   ""

Za příkazem x se tedy za lomítko zadává:

  1. Počet prvků.
  2. Formátovací znak o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string)
  3. Jak velké jsou prvky: b(byte), h(halfword), w(word), g(8 bajtů)

Velikost prvků se zadává, jen když je to nutné, protože GNU Debugger velikost mnohdy odvodí automaticky podle metadat.

12. Užitečný příkaz display

Při ladění mnohdy potřebujeme neustále sledovat obsah některých registrů či bloku operační paměti. Zadávat neustále příkaz print popř. info registers je samozřejmě těžkopádné a proto přichází na řadu další velmi užitečný příkaz display. Tomu lze zadat výraz, který se vyhodnotí a vypíše po každém provedeném kroku. Podívejme se na příklad krokování smyčky loop, v níž pracujeme s osmibitovým registrem al a 32bitovým registrem ecx:

(gdb) display $al
(gdb) display $ecx

Nastavíme breakpoint a spustíme náš testovací program:

(gdb) break loop
Breakpoint 1 at 0x4000b7: file test.s, line 36.
 
(gdb) run
Starting program: /home/tester/temp/a.out
 
Breakpoint 1, loop () at test.s:36
36              mov   [ecx], al              # zapis znaku do bufferu
2: $ecx = 6291688
1: $al = 97

Vidíme, že program se zastavil a současně se skutečně vypsaly i obsahy těch registrů, které nás zajímají. Proveďme další iteraci:

(gdb) c
Continuing.
 
Breakpoint 1, loop () at test.s:36
36              mov   [ecx], al              # zapis znaku do bufferu
2: $ecx = 6291689
1: $al = 98

A znovu…

(gdb) c
Continuing.
 
Breakpoint 1, loop () at test.s:36
36              mov   [ecx], al              # zapis znaku do bufferu
2: $ecx = 6291690
1: $al = 99

Přidejme ještě sledování bloku operační paměti, konkrétně našeho bufferu:

(gdb) display/50xb 0x6000e8
3: $ecx = 6291688
2: $al = 97
1: x/50xb 0x6000e8
0x6000e8 <buffer>:      0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f0 <buffer+8>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f8 <buffer+16>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600100 <buffer+24>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600108:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600110:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600118:       0x00    0x00
(gdb) c
Continuing.
 
Breakpoint 1, loop () at test.s:37
37              mov   [ecx], al              # zapis znaku do bufferu
3: $ecx = 6291689
2: $al = 98
1: x/50xb 0x6000e8
0x6000e8 <buffer>:      0x61    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f0 <buffer+8>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f8 <buffer+16>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600100 <buffer+24>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600108:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600110:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600118:       0x00    0x00
(gdb) c
Continuing.
 
Breakpoint 1, loop () at test.s:37
37              mov   [ecx], al              # zapis znaku do bufferu
3: $ecx = 6291690
2: $al = 99
1: x/50xb 0x6000e8
0x6000e8 <buffer>:      0x61    0x62    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f0 <buffer+8>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6000f8 <buffer+16>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600100 <buffer+24>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600108:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600110:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x600118:       0x00    0x00

Vidíme, že příkaz display je skutečně užitečný a může GDB přiblížit možnostem celoobrazovkových debuggerů či debuggerů s GUI.

13. Praktický příklad a použití TUI

Pro úplnost se ještě podívejme, jak vypadá použití TUI společně s programy laděnými na úrovni jednotlivých instrukcí. Pravděpodobně nejdůležitější je zde příkaz layout, kterým lze změnit zobrazené informace:

Obrázek 1: Po spuštění gdbtui se v okně zobrazí i původní zdrojový kód.

Obrázek 2: Přidali jsme okno s výpisem pracovních registrů příkazem layout regs. Současně se nastavil breakpoint, což je vidět i ze značky B+ se šipkou.

ict ve školství 24

Obrázek 3: Při krokování se zvýrazňuje jak právě zpracovávaná instrukce, tak i ty pracovní registry, jejichž obsah se změnil.

Obrázek 4: Při krokování se zvýrazňuje jak právě zpracovávaná instrukce, tak i ty pracovní registry, jejichž obsah se změnil.

14. Odkazy na Internetu

  1. Debuggery a jejich nadstavby v Linuxu
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu/
  2. Debuggery a jejich nadstavby v Linuxu (2. část)
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-2-cast/
  3. Debuggery a jejich nadstavby v Linuxu (3): Nemiver
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-3-nemiver/
  4. Debuggery a jejich nadstavby v Linuxu (4): KDbg
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-4-kdbg/
  5. Debuggery a jejich nadstavby v Linuxu (5): ladění aplikací v editorech Emacs a Vim
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-5-ladeni-aplikaci-v-editorech-emacs-a-vim/
  6. Tracing (software)
    https://en.wikipedia.org/wi­ki/Tracing_%28software%29
  7. ltrace(1) – Linux man page
    http://linux.die.net/man/1/ltrace
  8. ltrace (Wikipedia)
    https://en.wikipedia.org/wiki/Ltrace
  9. strace(1) – Linux man page
    http://linux.die.net/man/1/strace
  10. strace (stránka projektu na SourceForge)
    https://sourceforge.net/pro­jects/strace/
  11. strace (Wikipedia)
    https://en.wikipedia.org/wiki/Strace
  12. SystemTap (stránka projektu)
    https://sourceware.org/systemtap/
  13. SystemTap (Wiki projektu)
    https://sourceware.org/systemtap/wiki
  14. SystemTap (Wikipedia)
    https://en.wikipedia.org/wi­ki/SystemTap
  15. Dynamic Tracing with DTrace & SystemTap
    http://myaut.github.io/dtrace-stap-book/
  16. DTrace (Wikipedia)
    https://en.wikipedia.org/wiki/DTrace
  17. GDB – Dokumentace
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/
  18. GDB – Supported Languages
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/Suppor­ted-Languages.html#Supported-Languages
  19. GNU Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Debugger
  20. The LLDB Debugger
    http://lldb.llvm.org/
  21. Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/Debugger
  22. 13 Linux Debuggers for C++ Reviewed
    http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817
  23. Getting started with ltrace: how does it do that?
    https://www.ellexus.com/getting-started-with-ltrace-how-does-it-do-that/
  24. Reverse Engineering Tools in Linux – strings, nm, ltrace, strace, LD_PRELOAD
    http://www.thegeekstuff.com/2012/03/re­verse-engineering-tools/
  25. 7 Strace Examples to Debug the Execution of a Program in Linux
    http://www.thegeekstuff.com/2011/11/stra­ce-examples/
  26. Oracle® Solaris 11.3 DTrace (Dynamic Tracing) Guide
    http://docs.oracle.com/cd/E53394_01/html/E­53395/gkwpo.html#scrolltoc
  27. An Introduction To Using GDB Under Emacs
    http://tedlab.mit.edu/~dr/gdbin­tro.html
  28. GNU Emacs
    https://www.gnu.org/softwa­re/emacs/emacs.html
  29. The Emacs Editor
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­macs/index.html
  30. Emacs Lisp
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­lisp/index.html
  31. An Introduction to Programming in Emacs Lisp
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­intr/index.html
  32. 27.6 Running Debuggers Under Emacs
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­macs/Debuggers.html
  33. GdbMode
    http://www.emacswiki.org/e­macs/GdbMode
  34. Emacs (Wikipedia)
    https://en.wikipedia.org/wiki/Emacs
  35. Emacs Lisp (Wikipedia)
    https://en.wikipedia.org/wi­ki/Emacs_Lisp
  36. Pyclewn installation notes
    http://pyclewn.sourceforge­.net/install.html
  37. pip Installation
    https://pip.pypa.io/en/la­test/installing.html
  38. Clewn
    http://clewn.sourceforge.net/
  39. Clewn installation
    http://clewn.sourceforge.net/in­stall.html
  40. Clewn – soubory
    http://sourceforge.net/pro­jects/clewn/files/OldFiles/
  41. KDbg: úvodní stránka
    http://www.kdbg.org/
  42. Nemiver (stránky projektu)
    https://wiki.gnome.org/Apps/Nemiver
  43. Basic Assembler Debugging with GDB
    http://dbp-consulting.com/tutorials/de­bugging/basicAsmDebuggingGDB­.html
  44. Nemiver FAQ
    https://wiki.gnome.org/Ap­ps/Nemiver/FAQ
  45. Nemiver (Wikipedia)
    https://en.wikipedia.org/wiki/Nemiver
  46. Data Display Debugger
    https://www.gnu.org/software/ddd/
  47. GDB – Dokumentace
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/
  48. BASH Debugger
    http://bashdb.sourceforge.net/
  49. The Perl Debugger(s)
    http://debugger.perl.org/
  50. Visual Debugging with DDD
    http://www.drdobbs.com/tools/visual-debugging-with-ddd/184404519
  51. Pydb – Extended Python Debugger
    http://bashdb.sourceforge.net/pydb/
  52. Insight
    http://www.sourceware.org/insight/
  53. Supported Languages (GNU Debugger)
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/Suppor­ted-Languages.html#Supported-Languages
  54. GNU Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Debugger
  55. The LLDB Debugger
    http://lldb.llvm.org/
  56. Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/Debugger
  57. 13 Linux Debuggers for C++ Reviewed
    http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817
  58. Clewn
    http://clewn.sourceforge.net/
  59. Clewn installation
    http://clewn.sourceforge.net/in­stall.html
  60. Clewn – soubory ke stažení
    http://sourceforge.net/pro­jects/clewn/files/OldFiles/
  61. Pyclewn installation notes
    http://pyclewn.sourceforge­.net/install.html
  62. Debugging
    http://janus.uclan.ac.uk/pa­gray/labs/debug.htm

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.