GNU Debugger

Bu bölümde hata ayıklayıcı olarak, GNU sistemlerinde standart olan, GDB (GNU Debugger) uygulamasını inceleyeceğiz. gdb bir komut satırı uygulaması olarak çalışmakta ve C, C++, Objective-C, assembly ve Java olmak üzere bir çok dili desteklemektedir. gdb ayrıca, Eclipse, Qt Creator, NetBeans gibi bir çok IDE ve GNU DDD (Data Display Debugger) grafik önyüz uygulaması üzerinden de kullanılabilir.

gdb ile bir programın içsel durumunu inceleyebilir, işleyişine müdahale edebiliriz. Tipik olarak, kodu adım adım işletebilir, değişken ve yazmaçların değerlerini gözleyip değiştirebilir, kod üzerinde kesme noktaları belirleyebilir, akışa müdahale edebilir ve fonksiyonların çağrılma sırasını takip edebiliriz. gdb çok sayıda komut ve seçeneği barındırmasına karşın çoğu durumda göreli olarak daha az bir komut setiyle bir çok işi yapabilmekteyiz. Bu bölümde temel kullanım senaryolarına değinmeye çalışacağız.

GDB Kullanımı

İlk olarak, programların debug amaçlı olarak nasıl derlendiğine ve ardından gdb ile nasıl çalıştırıldığına bakalım.

Debug Amaçlı Derleme

Bir programın içsel durumunu etkin bir şekilde inceleyebilmek için çalışabilir dosya içinde debug sembolleri bulunmalıdır. Bu sembollerin bulunmaması durumunda gdb aşağıdaki gibi bir uyarı verecektir.

Reading symbols from debug...(no debugging symbols found)...done.

Uygulama içerisine debug sembollerini eklemek için, derleme sürecinde tipik olarak -g ve -ggdb anahtarları kullanılmaktadır. -ggdb anahtarı ile, -g anahtarından farklı olarak, yalnız gdb'nin kullanabileceği özel bazı semboller de üretilmektedir. Bu anahtarların ayrıca, fazladan bilgi üreten, seviye gösteren kullanımları da mevcuttur. Örneğin önişlemci makro tanımları için -g3 veya -ggdb3 anahtarları kullanılmalıdır. Makro kullanımına ilerideki bölümlerde değineceğiz.

Uygulamanın debug modda derlenmesi, çalıştırılabilir dosya formatına yeni alanları eklediğinden, kodun bir miktar büyümesine neden olacaktır. Aşağıdaki gibi basit bir kodu önce normal, sonrasında debug modda derleyerek karşılaştıralım.

int main() {
    int i = 111;
    return 0;
}

Kodu debug.c adıyla saklayıp sırasıyla aşağıdaki gibi derleyip, gcc tarafından üretilen dosyaları inceleyebilirsiniz.

$ gcc -odebug debug.c --save-temps
$ ls -lh debug.s
  -rw-r--r-- 1 root root 384 Şub  9 11:18 debug.s

$ readelf -S debug | wc -l
69

$ gcc -odebug debug.c --save-temps -g
$ ls -lh debug.s
  -rw-r--r-- 1 root root 2,5K Şub  9 11:18 debug.s

$ readelf -S debug | wc -l
79

$ readelf -S debug | grep -i debug
  [27] .debug_aranges    PROGBITS         0000000000000000  00001080
  [28] .debug_info       PROGBITS         0000000000000000  000010b0
  [29] .debug_abbrev     PROGBITS         0000000000000000  00001113
  [30] .debug_line       PROGBITS         0000000000000000  0000115b
  [31] .debug_str        PROGBITS         0000000000000000  00001197

Bu örnek için üretilen sembolik makina kodunun 384B'tan 2.5K'ya çıktığını ve çalışabilir dosya formatına yeni bölümlerin eklendiğini görmekteyiz.

Not: Linux altında, derleyicinin ürettiği amaç dosyalar, paylaşımlı kütüphaneler ve çalışabilir dosyalar ELF (Executable and Linkable Format) formatında saklanmaktadır. ELF formatı çok sayıda bölümden oluşmaktadır. readelf aracı ile ELF dosya formatını inceleyebilir, S anahtarı ile bölüm başlıklarını (section headers) listeleyebilirsiniz.

Programların GDB İle Çalıştırılması

İncelenecek olan programlar gdb ile çalıştırılabildiği gibi çalışan uygulamalar da, proses kimlikleri kullanılarak, gdb üzerinden incelenebilir. Uygulama isimlerini ve proses kimliklerini gdb'ye argüman olarak geçirebildiğimiz gibi aynı işlemleri gdb komut satırından da yapabiliriz. Tipik kullanımlar aşağıdaki gibidir.

Kullanım
$ gdb programismi
(gdb) file programismi
$ gdb -p proseskimliği
(gdb) attach proseskimliği

Program isminin belirtildiği, tablodaki ilk iki kullanım şeklinde, uygulama öncelikle gdb tarafından yüklenmektedir. Uygulamayı çalıştırmak için sonrasında run komutunu kullanabilirsiniz.

Not: gdb açılışta sahiplik bilgilerini de içeren bir karşılama mesajı basmaktadır. Komut satırından -q anahtarını kullanarak bu mesajın görüntülenmesini engelleyebilirsiniz.

Programlara Komut Satırı Argümanlarının Geçirilmesi

Komut satırı argümanlarını aşağıda gösterilen 3 farklı şekilde de geçirmek mümkündür.

$ gdb -q --args debug ARGUMENT
(gdb) set args ARGUMENT
(gdb) run ARGUMENT

Uygulamaya geçirilen argümanlar aşağıdaki gibi listelenebilir.

(gdb) show args
Argument list to give program being debugged when it is started is "ARGUMENT".

Uygulamanın Çalışmasının Sonlandırılması ve Askıya Alınması

kill komutu ile uygulama sonlandırabilir, Ctrl-C tuşlarıyla uygulamanın çalışmasını geçici olarak durdurabilirsiniz.

GDB'nin Sonlandırılması

gdb uygulamasından quit komutu veya Ctrl-D seçenekleriyle çıkabilirsiniz.

Yardım

Komut satırında help yazarak değişik seviyelerde yardım alabilirsiniz. Yalnız help yazarak genel yardım listesini alabilir, sonrasında ilgilendiğiniz komuta ulaşarak daha detaylı bilgi alabilirsiniz. Örnek bir kullanım aşağıdaki gibi olabilir.

(gdb) help
  List of classes of commands:

  aliases -- Aliases of other commands
  breakpoints -- Making program stop at certain points
  data -- Examining data
  ...

breakpoints kullanımı ile ilgili olduğumuzu var sayalım.

(gdb) help breakpoints 
  Making program stop at certain points.

  List of commands:

  awatch -- Set a watchpoint for an expression
  break -- Set breakpoint at specified line or function
  break-range -- Set a breakpoint for an address range
  ...

Nihayetinde gerçek break komut ile ilgili yardım alabiliriz.

(gdb) help break
  Set breakpoint at specified line or function.
  ...

Çoğu durumda, bir gdb komutunun tamamını yazmaksızın, sadece diğer komutlardan ayrım yapılabilecek kadar olan kısmını yazarak, komutu kullanabilirsiniz. Ayrıca tab tuşana basarak gdb'nin kodu tamamlamasını veya adayları listelemesini sağlayabilirsiniz. Örneğin aşağıdaki komutların hepsi aynı sonucu üretecektir.

(gdb) disas main
(gdb) disass main
(gdb) disasse main
(gdb) disassem main
(gdb) disassemb main
(gdb) disassembl main
(gdb) disassemble main

GDB Temel Özellikleri

Bu bölümde, gdb kullanarak, bir uygulama hakkında nasıl bilgi alabileceğimize ve işleyişine nasıl müdahale edebileceğimize bakacağız.

Kaynak Kodun Listelenmesi

Kaynak kod list komutu ile listelenebilir. Aldığı argümanlar ve temel kullanım şekilleri aşağıdaki gibidir.

Komut Açıklama
list Son listelenen noktadan ya da kodun başından itibaren 10 satırı görüntüler
list SATIRNUMARASI İstenilen noktayı çevreleyen 10 satırı listeler
list BAŞLANGIÇSATIRI,BİTİŞSATIRI Belirtilen başlangıç ve bitiş noktalarının arasını listeler
list FONKSİYONADI Belirtilen fonksiyonu görüntüler
list DOSYAADI:SATIRNUMARASI Projenin birden çok dosyadan oluşması durumunda satır numarasından önce dosya ismi belirtilebilir
list DOSYAADI:FONKSİYONADI Projenin birden çok dosyadan oluşması durumunda fonksiyon adından önce dosya ismi belirtilebilir

Makina Kodlarının Listelenmesi

Sembolik ve karşılık geldikleri gerçek makina kodları disassemble komutu ile listelenebilir. disassemble argüman olarak bellek adresi almaktadır, örnek kullanımları aşağıdaki gibidir.

Komut Açıklama
disas FONKSİYONADI Belirtilen fonksiyona ait sembolik makina kodlarını listeler
disas /m FONKSİYONADI Sembolik makina kodlarıyla beraber karşılık gelen kaynak kod satırları da listelenir
disas /r FONKSİYONADI Sembolik makina kodlarıyla beraber gerçek makina kodları da listelenir

Program Akışının İzlenmesi

Programın akışı, kullanıcı tarafından Ctrl-C tuşlarına basılarak veya önceden belirlenen kesme noktalarıyla durdurulabilir. Kesme noktalarının kullanımına ilerleyen bölümlerde değineceğiz. Durdurulan program sonrasında kaldığı yerden, kullanıcı müdahalesi olmaksızın, yoluna devam edebileceği gibi kontrollü bir şekilde adım adım da çalıştırılabilir.

Akış komutları ve kullanım şekilleri aşağıdaki gibidir.

Komut Açıklama
continue Akış kaldığı yerden devam ettirilir
next [N] 1Aldığı argüman sayısınca, fonksiyon çağrılarını tek satır olarak ele alarak, kodu kaynak kod düzeyinde satır satır çalıştırır
step [N] Aldığı argüman sayısınca, çağrılan fonksiyonların içine girerek, kodu kaynak kod düzeyinde satır satır çalıştırır
nexti [N] Aldığı argüman sayısınca, fonksiyon çağrılarını tek satır olarak ele alarak, makina kodlarını adım adım çalıştırır
stepi [N] Aldığı argüman sayısınca, çağrılan fonksiyonların içine girerek, makina kodlarını adım adım çalıştırır

next ve step komutları arasındaki, ister programın yazıldığı kaynak kod düzeyinde ister sembolik makina komutları düzeyinde olsun, fonksiyon çağrılarını ele alış biçimlerindeki farklılığa dikkat ediniz. next komutlarında fonksiyonlar tek hamlede işletilmekte buna karşın step komutlarında akış çağrı yapılan fonksiyon kodundan devam etmektedir. Aradaki farkı görmek için basit bir örnek yapalım. Aşağıdaki kodu debug.c adıyla saklayıp derleyebilirsiniz.

#include <stdio.h>

void foo() {
    int i;
    for (i = 0; i < 5; ++i) {
        puts(__func__);    
    }
}

int main(int argc, char **argv) {
    foo();
    return 0;
}
$ gcc -odebug debug.c -m32 -g

Şimdi gdb'yi uygulamamız için çalıştıralım.

$ gdb -q debug
Reading symbols from debug...done.
(gdb)

Program akışı main fonksiyonunda kesilecek şekilde, break komutu ile, bir kesme noktası tanımlayalım ve uygulamayı çalıştıralım. break komutunun detaylarına ilerleyen bölümlerde bakacağız.

(gdb) break main
Breakpoint 1 at 0x8048457: file debug.c, line 11.
(gdb) run
Starting program: /home/serkan/embedded/gdb/debug 

Breakpoint 1, main (argc=1, argv=0xffffd574) at debug.c:11
11              foo();

Akışın 11. satırda yani foo fonksiyonu çağrısında durduğunu görüyoruz. Daha detaylı inceleme yapabilmek için, disassemble komutuyla, sembolik makina kodlarına bakalım.

(gdb) disas
    Dump of assembler code for function main:
      0x08048446 <+0>:     lea    0x4(%esp),%ecx
      0x0804844a <+4>:     and    $0xfffffff0,%esp
      0x0804844d <+7>:     pushl  -0x4(%ecx)
      0x08048450 <+10>:    push   %ebp
      0x08048451 <+11>:    mov    %esp,%ebp
      0x08048453 <+13>:    push   %ecx
      0x08048454 <+14>:    sub    $0x4,%esp
   => 0x08048457 <+17>:    call   0x804841b <foo>
      0x0804845c <+22>:    mov    $0x0,%eax
      0x08048461 <+27>:    add    $0x4,%esp
      0x08048464 <+30>:    pop    %ecx
      0x08048465 <+31>:    pop    %ebp
      0x08048466 <+32>:    lea    -0x4(%ecx),%esp
      0x08048469 <+35>:    ret    
End of assembler dump.

Sembolik makina kodlarındaki ok işareti, henüz işletilmemiş, sıradaki ilk komutu göstermektedir. nexti ile kodun işleyişini bir adım ilerletelim.

Not: Kesme noktasının gösterdiği makina komutunun fonksiyonun ilk makina komutu değil, derleyici tarafından yazılan başlangıç kodlarını (prologue) takip eden, foo fonksiyonu çağrı komutu olduğuna dikkat ediniz.

(gdb) nexti
foo
foo
foo
foo
foo
12              return 0;

Tekrar sembolik makina kodlarına bakalım.

(gdb) disas
    Dump of assembler code for function main:
      0x08048446 <+0>:     lea    0x4(%esp),%ecx
      0x0804844a <+4>:     and    $0xfffffff0,%esp
      0x0804844d <+7>:     pushl  -0x4(%ecx)
      0x08048450 <+10>:    push   %ebp
      0x08048451 <+11>:    mov    %esp,%ebp
      0x08048453 <+13>:    push   %ecx
      0x08048454 <+14>:    sub    $0x4,%esp
      0x08048457 <+17>:    call   0x804841b <foo>
   => 0x0804845c <+22>:    mov    $0x0,%eax
      0x08048461 <+27>:    add    $0x4,%esp
      0x08048464 <+30>:    pop    %ecx
      0x08048465 <+31>:    pop    %ebp
      0x08048466 <+32>:    lea    -0x4(%ecx),%esp
      0x08048469 <+35>:    ret    
    End of assembler dump.

nexti komutuyla foo fonksiyonunun işletildiğini ve akışın main fonksiyonun bir sonraki komutundan devam ettirildiğini görüyoruz. Şimdi benzer işlemi stepi komutuyla yapalım. Öncesinde kill komutuyla uygulamayı sonlandırıp uygulamayı yeniden çalıştıralım.

(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) run
Starting program: /home/serkan/embedded/gdb/debug 

Breakpoint 1, main (argc=1, argv=0xffffd574) at debug.c:11
11              foo();

Bu sefer stepi ile kodun işleyişini bir adım ilerletelim.

(gdb) stepi
foo () at debug.c:3
3       void foo() {

stepi komutu ile foo fonksiyonu tek hamlede çalıştırılmak yerine akış foo fonksiyonuna dallanmaktadır. Sembolik makina kodlarına baktığımızda bu durum daha açık şekilde gözükmektedir.

(gdb) disas
    Dump of assembler code for function foo:
   => 0x0804841b <+0>:     push   %ebp
      0x0804841c <+1>:     mov    %esp,%ebp
      0x0804841e <+3>:     sub    $0x18,%esp
      0x08048421 <+6>:     movl   $0x0,-0xc(%ebp)
      0x08048428 <+13>:    jmp    0x804843e <foo+35>
      0x0804842a <+15>:    sub    $0xc,%esp
      0x0804842d <+18>:    push   $0x8048500
      0x08048432 <+23>:    call   0x80482f0 <[email protected]>
      0x08048437 <+28>:    add    $0x10,%esp
      0x0804843a <+31>:    addl   $0x1,-0xc(%ebp)
      0x0804843e <+35>:    cmpl   $0x4,-0xc(%ebp)
      0x08048442 <+39>:    jle    0x804842a <foo+15>
      0x08048444 <+41>:    leave  
      0x08048445 <+42>:    ret    
    End of assembler dump.

Fonksiyonların Çağrılması

Program kodundaki veya programa linklenmiş dinamik kütüphane içeriğindeki fonksiyonları call komutuyla çağırabilirsiniz. finish komutuyla da bir fonksiyonu sonlandırarak çağıran fonksiyona geri dönülebilir.

Program Verisinin İncelenmesi

Değişkenlerle temsil edilen veya direkt adres kullanılarak gösterilen bellek alanlarını, print ve x komutlarıyla inceleyebiliriz, ayrıca bu bölümde yazmaç değerlerini nasıl elde edebileceğimize de bakacağız. Tam olarak aynı işi yapmamalarına karşın, birbirinin yerine geçebilen kullanımları olan bu komutlara daha yakından bakalım.

Komut Açıklama
print /FORMAT İFADE Programın yazıldığı kaynak kod düzeyindeki anlamlı bir ifadeyi belirtilen formatta gösterir
print /FORMAT ADRESGÖSTERENDEĞ[email protected] İfadenin bir adres göstermesi durumunda, @ operatoru ile devam eden N-1 adet bellek bölgesi de, tür bilgisi gözetilerek, gösterilir

Bu noktada bir ifadeye ait tür bilgisinin whatis komutuyla öğrenebildiğini söyleyelim.

Şimdi print komutunun kullanımına bakalım, aşağıdaki örnek kodu debug.c adıyla saklayıp derleyebilirsiniz.

#include <stdio.h>

int main(int argc, char **argv) {
    const char *str = "zeytin";
    int i = 111;
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    return 0;
}
$ gcc -odebug debug.c -g

gdb'yi çalıştırdıktan sonra, ilgilendiğimiz yerel değişkenlerin sonrasına bir kesme noktası koyalım ve programı çalıştıralım. Bu sayede bu program sonlanmayacak ve incelemelerimize devam edebileceğiz.

$ gdb -q debug
Reading symbols from debug...done.
(gdb) list
1       #include <stdio.h>
2
3       int main(int argc, char **argv) {
4               const char *str = "zeytin";
5               int i = 111;
6               int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
7               return 0;
8       }
(gdb) br 7
Breakpoint 1 at 0x80484b3: file debug.c, line 7.
(gdb) run
Starting program: /home/serkan/embedded/gdb/debug 

Breakpoint 1, main (argc=1, argv=0xffffd574) at debug.c:7
7               return 0;

Sırasıyla s, i ve arr yerel değişkenleri için bazı örnekler yapalım.

Not: Normal bir derleme sürecinde, değişmez adresleri olan statik ömürlü değişkenlerin aksine, konumları yazmaç göreli olarak değişen yerel değişken isimleri derleyici tarafından üretilen amaç kod (object code) içinde saklanmazlar. Debug hedefli derleme yaparak yerel değişken isimlerini kullanabildiğimize dikkat ediniz.

(gdb) print str
$2 = 0x8048570 "zeytin"
(gdb) print i
$3 = 111
(gdb) print &i
$4 = (int *) 0xffffd490
(gdb) print *0xffffd490
$5 = 111
(gdb) print /x i
$6 = 0x6f

(gdb) print arr
$7 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb) print arr[0]
$8 = 0
(gdb) print arr[0]@5
$9 = {0, 1, 2, 3, 4}
(gdb) print [email protected]
$10 = {111, 0, 1, 2, 3}
(gdb) print /x [email protected]
$11 = {0x6f, 0x0, 0x1, 0x2, 0x3}

Şimdi belleği incelemek için kullanabileceğimiz bir diğer komut olan x (Examine memory) komutuna bakalım.

Komut Açıklama
x /FORMAT ADRESİFADESİ Belirtilen adresteki bellek bölgesinin değeri formatlı bir şekilde gösterilir

Şimdi x komutuyla bazı basit örnekler yapalım.

(gdb) x /c str
0x8048570:      122 'z'
(gdb) x /s str
0x8048570:      "zeytin"
(gdb) x &i
0xffffd490:     111

Yazmaç Değerlerinin Elde Edilmesi

Yazmaç değerlerini print ve info komutlarıyla almak mümkündür. Genel şekilleri ve örnekler aşağıdaki gibidir.

Komut
print /FORMAT $YAZMAÇ
info registers
 (gdb) info registers
  eax            0x0      0
  ecx            0xffffd4e0       -11040
  edx            0xffffd504       -11004
  ebx            0xf7fb1000       -134541312
  esp            0xffffd470       0xffffd470
  ebp            0xffffd4c8       0xffffd4c8
  esi            0x0      0
  edi            0x0      0
  eip            0x80484b3        0x80484b3 <main+120>
  eflags         0x246    [ PF ZF IF ]
  cs             0x23     35
  ss             0x2b     43
  ds             0x2b     43
  es             0x2b     43
  fs             0x0      0
  gs             0x63     99
  (gdb) info registers eip
  eip            0x80484b3        0x80484b3 <main+120>
  (gdb) info registers eax
  eax            0x0      0
  (gdb) print /x $eax
  $29 = 0x0
  (gdb) print /x $eip
  $30 = 0x80484b3

Not: Format karakterlerinin çoğu, C dilinden aşina olduğumuz karakterlerden oluşmaktadır, tam liste için help komutundan faydalanabilirsiniz.

Program Verisinin Değiştirilmesi

Bellek alanlarının ve yazmaçların değerlerini set komutuyla değiştirebilirsiniz. set komutu çok sayıda alt komuta (subcommand) sahiptir, tam liste için help set şeklinde yardım alabilirsiniz. Genel kullanım şekli ve bir önceki kod için örnekler aşağıdaki gibidir.

Komut
set var GÜNCELLENECEKALAN=İFADE
(gdb) print i
$1 = 111
(gdb) print &i
$2 = (int *) 0xffffd490
(gdb) set var i = 33
(gdb) print i
$3 = 33
(gdb) set var *(unsigned int*)0xffffd490 = 44
(gdb) print i
$4 = 44
(gdb) print /x $eax
$5 = 0x0
(gdb) set var $eax = 55
(gdb) print $eax
$6 = 55
(gdb) print /s str
$7 = 0x8048570 "zeytin"
(gdb) set var *(char *)0x8048570 = 'm'
(gdb) print /s str
$8 = 0x8048570 "meytin"

Geçmiş Değerlerin Kullanımı

gdb, print komutunun ürettiği sonuçları, geriye dönük takip edebilmek ve yeniden kullanabilmek için, $NUM, şeklinde numaralandırdığı değişkenlerde saklamaktadır. show values komutuyla geçmiş değerleri listeleyebilirsiniz.

(gdb) print i
$15 = 555
(gdb) print $15
$16 = 555
(gdb) print $15
$16 = 555
(gdb) show values
$7 = 0x8048570 "meytin"
$8 = 0x8048570 "meytin"
$9 = 44
$10 = 44
$11 = 111
$12 = (int *) 0xffffd490
$13 = 33
$14 = 44
$15 = 555
$16 = 555

Macro İşlemleri

Önişlemci makrolarına ilişkin bilgileri de debug bilgisi olarak saklayabilmek için gcc'ye g3 veya ggdb3 anahtarlarından birini geçirmeliyiz. Aşağıdaki örnek üzerinden macro işlemlerini nasıl yapabileceğimize bakalım. Kodu debug.c olarak saklayıp derleyebilirsiniz.

#define NUMBER 111

int main() {
    return 0;
}
$ gcc -odebug debug.c -g3

Program çalışmıyor iken bile list komutuyla makro tanımlarının üzerinden geçip sonrasında makro değerlerlerine ulaşabiliriz.

Makroların karşılık geldiklerini değerleri öğrenmek için macro expand veya info macro komutlarını kullanabilirsiniz. Ayrıca macro define ve macro undef ile makro tanımlayabilir veya geçersiz kılabilirsiniz. help macro komutuyla makro işlemleriyle ilgili yardım alınabilir.

Tanımladığımız NUMBER makrosunun değerine iki farklı şekilde ulaşabiliriz.

$ gdb -q debug
Reading symbols from debug...done.
(gdb) list 1,5
1       #define NUMBER 111
2
3       int main() {
4               return 0;
5       }
(gdb) macro expand NUMBER
expands to: 111
(gdb) info macro NUMBER
Defined at /home/serkan/embedded/gdb/debug.c:1
#define NUMBER 111

Yığınının (Call Stack) İncelenmesi

Bir fonksiyon çağrıldığında, yığında fonksiyon için, yığın çerçevesi (call frame) olarak isimlendirilen ve fonksiyon sonlandığında geri verilen, yeni bir alan ayrılır. Fonksiyona geçirilen argümanlar, yerel değişkenler ve fonksiyonun geri dönüş adresi yığın çerçevesinde bulunmaktadır. Yığının, son girenin ilk çıktığı (LIFO) bir veri alanı olduğunu hatırlayınız. gdb yığın üzerinde işlem yapabilmek için gerekli komutları barındırmaktadır. Şimdi bu komutların genel kullanımına bakalım.

Komut Açıklama
backtrace [N] [full] Argümansız kullanılması durumunda tüm yığın çerçevelerini görüntüler. N değerinin pozitif olması durumunda en içteki N adet, negatif olması durumunda ise en dıştaki N adet çerçeve listelenir. full niteleyicisi geçirilmesi durumunda ise yerel değişken değerleri de listelenir
frame [N] Argümansız kullanıması durumunda gündemdeki (current) yığın çerçevesine ait bilgileri görüntüler. Argümanlı kullanımda belirtilen çerçeveyi seçer
info frame [N] Gündemdeki veya N argümanıyla belirtilen yığın çerçevesine ait bilgileri görüntüler
info locals Gündemdeki yığın çerçevesine ait yerel değişken değerlerini görüntüler

İncelememize bir örnek üzerinden devam edelim, aşağıdaki örneği debug.c ismiyle saklayıp derleyebilirsiniz.

void bar() {
    int k = 111;
}

void foo() {
    int j = 111;
    bar();
}

int main() {
    int i = 111;
    foo();
    return 0;
}
$ gcc -odebug debug.c -g

bar fonksiyonuna bir kesme noktası koyarak uygulamayı çalıştıralım ve yığın çerçevelerine bakalım.

$ gdb -q debug
Reading symbols from debug...done.
(gdb) break bar
Breakpoint 1 at 0x4004fa: file debug.c, line 2.
(gdb) run
Starting program: /home/serkan/embedded/gdb/debug 

Breakpoint 1, bar () at debug.c:2
2               int k = 111;
(gdb) backtrace
#0  bar () at debug.c:2
#1  0x000000000040051c in foo () at debug.c:7
#2  0x0000000000400537 in main () at debug.c:12

backtrace komutunun, içten dışa doğru, yığın çerçevelerini numaralandırarak listelediğini görmekteyiz. # karakteriyle başlayan bu numaralar yığın çerçevelerini tanımlamak için kullanılmaktadır, daha sonra frame komutunda da bu numaraları kullanacağız. backtrace komutuna, ilk veya son yığın çerçevesinden başlayarak, listelemesini istediğimiz çerçeve sayısını argüman geçirebiliriz, aşağıdaki kullanımları inceleyiniz.

(gdb) bt 1
#0  bar () at debug.c:2
(More stack frames follow...)
(gdb) bt -1
#2  0x0000000000400537 in main () at debug.c:12
(gdb)
(gdb) bt 2
#0  bar () at debug.c:2
#1  0x000000000040051c in foo () at debug.c:7
(More stack frames follow...)
(gdb) bt -2
#1  0x000000000040051c in foo () at debug.c:7
#2  0x0000000000400537 in main () at debug.c:12

Şimdi frame ve info locals komutlarının kullanımlarına bakalım. frame ile isteğimiz bir çerçeveyi seçebilir ve info locals ile o çerçeveye ait yerel değişken değerlerine ulaşabiliriz.

(gdb) frame
#0  bar () at debug.c:2
2               int k = 111;
(gdb) info locals
k = 0
(gdb) frame 2
#2  0x0000000000400537 in main () at debug.c:12
12              foo();
(gdb) info locals
i = 111

Komut Dosyaları

Komut dosyaları, gdb komutlarından oluşan yazı dosyalarıdır. # ile başlayan satırlar açıklama olarak ele alınır. gdb çalıştırılırken, komut dosyası argüman olarak geçirilebildiği gibi sonrasında gdb komut satırından da gösterilebilir. Genel kullanımları aşağıdaki gibidir.

Kullanım
$ gdb -x KOMUTDOSYASI
(gdb) source KOMUTDOSYASI

KOMUTDOSYASI dosya ismi ve dizin yolundan oluşabilir. Şimdi, incelediğimiz debug programı için, basit bir örnek yapalım. Aşağıdaki komutları commands.txt dosyasında saklayabilir ve sonrasında gdb'yi aşağıdaki gibi çalıştırabilirsiniz.

# Komut dosyası
# İlk olarak debug programı yüklenir, kaynak kod listelendikten sonra
# bar fonksiyonuna kadar kod işletilir.

file debug
list
break bar
run
$ gdb -q -x commands.txt
1       void bar() {
2               int k = 111;
3       }
4
5       void foo() {
6               int j = 111;
7               bar();
8       }
9
10      int main() {
Breakpoint 1 at 0x4004fa: file debug.c, line 2.

Breakpoint 1, bar () at debug.c:2
2               int k = 111;
(gdb)

Başlangıç Dosyaları

gdb ilk açılışta bazı öntanımlı dosyaları, bir öncelik sırasına göre, aramaktadır. Aradığı başlangıç dosyaları ve öncelikleri aşağıdaki gibidir.

Dosya Konum
system.gdbinit Kullanıcı veya çalışma dizininden bağımsız, sistem genelindeki başlangıç dosyasıdır. Bu özelliğin kullanılabilmesi için gdb --with-system-gdbinit seçeneği ile derlenmiş olmalıdır
~/.gdbinit Kullanıcı dizinindeki başlangıç dosyasıdır
./.gdbinit Çalışma dizinindeki başlangıç dosyasıdır

Daha önce komut dosyalarını incelerken kullandığımız örneği şimdi ana dizinde bir .gdbinit dosyası oluşturarak tekrarlayalım. Bu durumda gdb'yi çalıştırdığımızda aşağıdaki gibi başlayan bir uyarı güvenlik uyarısı alıyoruz.

$ gdb -q
warning: File "/home/serkan/embedded/gdb/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
    add-auto-load-safe-path /home/serkan/embedded/gdb/.gdbinit
...

Bu durumda kullanıcı dizinindeki .gdbinit dosyasına, uyarıda belirtilen add-auto-load-safe-path /home/serkan/embedded/gdb/.gdbinit komutunu ekleyip gdb'yi yeniden çalıştırtığımızda, çalışma dizinindeki .gdbinit dosyasının içeriğinin okunduğunu görmekteyiz.

$ gdb -q
1       void bar() {
2               int k = 111;
3       }
4
5       void foo() {
6               int j = 111;
7               bar();
8       }
9
10      int main() {
Breakpoint 1 at 0x4004fa: file debug.c, line 2.

Breakpoint 1, bar () at debug.c:2
2               int k = 111;

Çalışma Modları

Komut satırı seçeneklerini kullanarak gdb'yi farklı modlarda çalıştırmak mümkündür. Desteklenen modlardan birkaçına bakalım.

Seçenek Mod
-nx Başlangıç dosyalarındaki komutlar çalıştırılmaz
-q Başlangıç mesajları basılmaz
-batch Kullanıcıyla etkileşime geçilmez, başlangıç dosyaları ve komut satırı seçenekleri işletildikten sonra gdb sonlanır

batch mod ile, istediğiniz komutları seçenek olarak geçirerek, gdb komut satırına düşmeksizin gdb'nin istediğiniz işleri yapmasını sağlayabilirsiniz. Çalışmasını istediğimiz komutları ex seçeneğiyle belirtebiliriz. Bu şekilde gdb'nin ürettiği çıktıyı bu şekilde bir dosyaya yönlendirebiliriz. Aşağıdaki örneği inceleyiniz.

$ gdb -q -batch -ex "file debug" -ex "list" -ex "break bar" -ex "run" > debug.txt

$ cat debug.txt
1       void bar() {
2               int k = 111;
3       }
4
5       void foo() {
6               int j = 111;
7               bar();
8       }
9
10      int main() {
Breakpoint 1 at 0x4004fa: file debug.c, line 2.

Breakpoint 1, bar () at debug.c:2
2               int k = 111;

Kabuk Kullanımı

gdb çalışıyorken, gdb'yi sonlandırmadan ya da askıya almadan, kabuk komutlarını shell KOMUT şeklinde çalıştırmak mümkündür. Örneğin, gdb ekranı temizlemek için bir komuta sahip olmadığından ekranı shell clear şeklinde temizleyebilirsiniz.

Kesme Noktaları Oluşturulması

Kesme noktaları, program akışının durdurulduğu ve kontrolün kullanıcıya geçtiği noktalardır. Bu durumda tipik kullanım, program verisini incelemek ve kodu, kontrollü bir şekilde, adım adım çalıştırmak şeklinde olmaktadır. Kesme noktaları program kodu üzerinde açık bir şekilde belirtilebildiği gibi data belleği üzerindeki alanlar da izlenebilir. Şimdi bu özelliğe daha yakından bakalım.

Kod Üzerinde Oluşturulan Kesme Noktaları (Breakpoints)

Program kodu üzerinde kesme noktası break KONUM şeklinde tanımlanmaktadır. Konum değeri aşağıdaki biçimlerde olabilmekte ayrıca bir koşul ifadesi de eklenebilmektedir.

Komut Açıklama
break [DOSYAADI:]SATIRNUMARASI Belirtilen satır kesme noktası olarak işaretlenir
break [DOSYAADI:]FONKSİYONADI Belirtilen fonksiyon kesme noktası olarak işaretlenir
break ADRES Belirtilen adres kesme noktası olarak işaretlenir
break KONUM if KOŞUL Kesme noktasına koşul eklenir

Proje birden çok kaynak dosyadan oluşuyorsa DOSYAADI belirtilmedir. Şimdi bir örnek üzerinde koşul içeren bir kesme noktası oluşturalım. Döngü içerisinde indeks değerinin basıldığı 6. satırı kesme noktası olarak belirliyoruz. Bir koşul belirtmeseydik, döngünün her turunda akış kesilecekti. Koşulu indeks değerinin 5 olması olarak belirlediğimizden akış bu kesme noktasında durduğunda i değişkeni 5 değerine sahiptir.

#include <stdio.h>

int main() {
    int i;
    for (i = 0; i < 10; ++i) {
        printf("%d\n", i);
    }
    return 0;
}
$ gcc -odebug debug.c -g
$ gdb -q debug
Reading symbols from debug...done.
(gdb) list
1       #include <stdio.h>
2
3       int main() {
4               int i;
5               for (i = 0; i < 10; ++i) {
6                       printf("%d\n", i);
7               }
8               return 0;
9       }
(gdb) break 6 if i == 5
Breakpoint 1 at 0x400547: file debug.c, line 6.
(gdb) run
Starting program: /home/serkan/embedded/gdb/debug 
0
1
2
3
4

Breakpoint 1, main () at debug.c:6
6                       printf("%d\n", i);
(gdb) print i
$1 = 5

Ayrıca, bir kesme noktasına sonradan da koşul ekleyebilirsiniz. Aynı örnek için 6 numaralı satıra kesme noktası ekleyip, sonrasında condition komutu ile bir koşul ifadesi ekleyebiliriz. condition komutunun genel hali aşağıdaki gibidir.

Komut Açıklama
condition N KOŞUL N kesme noktası numarasını göstermektedir

Tanımlanmış kesme numaralarını info breakpoints komutu ile öğrenebilirsiniz.

$ gdb -q debug
Reading symbols from debug...done.
(gdb) break 6
Breakpoint 1 at 0x400547: file debug.c, line 6.
(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400547 in main at debug.c:6
(gdb) condition 1 i == 5
(gdb) run
Starting program: /home/serkan/embedded/gdb/debug 
0
1
2
3
4

Breakpoint 1, main () at debug.c:6
6                       printf("%d\n", i);

Bellek Üzerinde Oluşturulan İzleme Noktaları (Watchpoints)

Kod üzerinde tanımlanan kesme noktalarının aksine izleme noktaları veri belleği üzerindedir. Bu sayede, veri belleği üzerinde ilgilendiğimiz bir alanın değeri okunduğunda veya değiştirildiğinde akışın durmasını sağlayabiliriz. İzleme noktaları oluşturduğumuzda, kod üzerinde ilgilendiğimiz bellek alanının değerini kullanan kısımları bulma zorunluluğumuz ortadan kalkmaktadır.

İzleme noktası oluşturmak için kullanılan komutlar aşağıdaki gibidir.

Komut Açıklama
watch İFADE Argüman olarak geçirilen ifadenin değeri değiştirildiğinde etkin olur
rwatch İFADE Argüman olarak geçirilen ifadenin değeri okunduğunda etkin olur
awatch İFADE Argüman olarak geçirilen ifadenin değeri okunduğunda veya değiştiğinde etkin olur

Genel biçimde gösterilen ifade çoğunlukla bir değişken adı olmaktadır. Bu özelliğin kullanımına basit bir örnek yaparak daha yakından bakalım.

int main() {
      int i;
      int j;
      i = 0;
      j = i;
      return 0;
}
$ gcc -odebug debug.c -g -m32

İlk olarak programı main fonskiyonuna kadar çalıştıralım, ardından i değişkenindeki değerin değişimini izlemek için watch i komutuyla bir izleme noktası oluşturduktan sonra continue ile programın çalışmasını devam ettirelim. Bu durumda programın i değerine ilişkin eski ve yeni değerleri vererek sonlandığını görüyoruz, disas ile i değişkenine (-0x8(%ebp)) 0 değerinin yazıldığını ve akışın sonraki makina komutunda durduğunu görüyoruz. Benzer testleri rwatch ve awatch komutlarıyla da yapabilirsiniz.

$ gdb -q debug
    Reading symbols from debug...done.
    (gdb) break main 
    Breakpoint 1 at 0x80483f1: file debug.c, line 6.
    (gdb) run
    Starting program: /home/serkan/embedded/gdb/debug 

    Breakpoint 1, main () at debug.c:6
    6               i = 0;
    (gdb) watch i
    Hardware watchpoint 2: i
    (gdb) c
    Continuing.
    Hardware watchpoint 2: i

    Old value = 134513680
    New value = 0
    main () at debug.c:7
    7               j = i;
    (gdb) disas
    Dump of assembler code for function main:
      0x080483eb <+0>:     push   %ebp
      0x080483ec <+1>:     mov    %esp,%ebp
      0x080483ee <+3>:     sub    $0x10,%esp
      0x080483f1 <+6>:     movl   $0x0,-0x8(%ebp)
    =>0x080483f8 <+13>:    mov    -0x8(%ebp),%eax
      0x080483fb <+16>:    mov    %eax,-0x4(%ebp)
      0x080483fe <+19>:    mov    $0x0,%eax
      0x08048403 <+24>:    leave  
      0x08048404 <+25>:    ret    
    End of assembler dump.

Burada bir noktaya dikkatinizi çekmek istiyoruz. İzleme noktası oluşturduğumuzda watch komutunun, Hardware watchpoint 2: i şeklinde bir bilgi mesaji verdiğini görmekteyiz. Mesajdaki Hardware ifadesi izleme noktasının donanım desteği alınarak oluşturulduğunu göstermektedir. Bir sonraki bölümde yazılımsal ve donanımsal olarak oluşturulan izleme noktalarına kısaca değineceğiz.

Yazılımsal ve Donanımsal Kesme Noktaları

Bir önceki bölümde, kod ve bellek üzerinde, kesme noktalarının nasıl kullanıldığını inceledik. Kod üzerinde belirlediğimiz noktalar işletilmeye çalışıldığında veya izlediğimiz değişkenler adreslendiğinde, akışın sonlandırıldığını ve kontrolun debugger programına, gdb, geçtiğini gördük. Şimdi bu özelliğin nasıl gerçekleştirildiğe daha yakından bakalım. Kesme noktaları yazılımsal ve donanımsal olmak üzere iki farklı şekilde oluşturulabilmektedir.

Yazılımsal Kesme Noktaları

Yazılımsal kesme noktaları, kod üzerinde oluşturduğumuz kesme noktalarını (breakpoints) oluşturmak için kullanılmaktadır. gdb uygulamasında bu noktaları break komutuyla oluşturmuştuk. Kod üzerinde yazılımsal kesme noktaları oluşturmanın birden çok yolu olmasına karşın burada gdb tarafından da kullanılan yöntemden bahsedeceğiz. Bu yöntemde, kesme noktasındaki makina komutu debugger tarafından değiştirilerek, işlemcinin bir istisna (exception) durumu oluşturması hedeflenmektedir. İstisna durumu oluştuğunda, işlemci normal akışını sonlandıracak ve işletim sistemi tarafından tanımlanan bir ele alım kodunu (interrup handler) çalıştıracaktır. Ele alım kodunun tipik davranışı ise bu duruma neden olan prosese bir sinyal göndermek şeklindedir. gdb, işletim sistemi tarafından gönderilen bu sinyalden haberdar olmakta ve kontrol gdb uygulamasına geçmektedir. gdb, alt proses olarak çalıştırdığı veya daha sonradan bağlandığı prosese gelen sinyalleri takip edebilmektedir. x86 mimarisinde, istisna durumu oluşturmak için, gerçek makina kodu 0xCC olan, int 3 sembolik makina kodu kullanılmaktadır. int sembolik makina kodu, yazılım yoluyla kesme (software interrupt) oluşturmak için kullanılmakta ve kesme numarasını operand olarak almaktadır. Normalde komutun kendisi (opcode) ve aldığı operand olmak üzere 2 byte olmasına karşın, int 3 komutu özel olarak bir byte ile gösterilmektedir. Bu durum, Intel Architecture Software Developer's Manual dokümanında aşağıdaki gibi ifade edilmiştir.

The INT 3 instruction generates a special one byte opcode (CC) that is intended for calling the debug exception handler. (This one byte form is valuable because it can be used to replace the first byte of any instruction with a breakpoint, including other one byte instructions, without over-writing other code).

İşlemci, int 3 sembolik makina koduyla karşılaştığında normal akışından çıkacak ve işletim sisteminin debug ele alım kodunu çalıştıracaktır. Bu kodda, nihayetinde debugger tarafından alınacak, SIGTRAP sinyalini üretecektir. gdb uygulamasında, bir kesme noktası belirlendiğinde karşılık geldiği konumdaki makina komutunun ilk byte değerinin 0xCC değeri ile değiştirildiğini söyledik. Bu durumda incelediğiniz kod üzerinde bir kesme noktası belirleyip sonrasında disas ile makina kodlarına baktığınızda, kesme noktasında, 0xCC değerini görmeyi bekleyebilirsiniz. Fakat gdb, gerçekte bu işlemi yapmasına karşın, disas çıktısında kodun değişmemiş halini göstermektedir. Bu yüzden bu durumu gözleyebilmek için, kendi makina kodlarının bir kısmını basan, aşağıdaki örneği kullanacağız. Örnek kodu break.c adıyla saklayıp, debug sembolleri olmaksızın, aşağıdaki gibi derleyebilirsiniz.

  #include <stdio.h>
  int main() {
      int i,j;
      unsigned char *p = (unsigned char*)main;

      for (j = 0; j < 2; j++) {
      printf("%p: ", p);
      for (i = 0; i < 16; i++)
          printf("%.2x ", *p++);
      printf("\n");
      }
      return 0;
  }
$ gcc -obreak break.c

İlk olarak programı, herhangi bir kesme noktası oluşturmaksızın, çalıştıralım. Çıktı olarak, main fonksiyonundan itibaren toplamda 32 byte'tan oluşan makina komutlarını görmekteyiz.

$ gdb -q break
  Reading symbols from break...(no debugging symbols found)...done.
  (gdb) run
  Starting program: /home/serkan/embedded/gdb/break 
  0x400586: 55 48 89 e5 48 83 ec 10 48 c7 45 f8 86 05 40 00 
  0x400596: c7 45 f4 00 00 00 00 eb 5a 48 8b 45 f8 48 89 c6 
  [Inferior 1 (process 22438) exited normally]

Sonrasında main fonksiyonunu kesme noktası olarak belirleyelim ve kodu main fonksiyonuna kadar işletelim.

  (gdb) break main
  Breakpoint 1 at 0x40058a
  (gdb) run
  Starting program: /home/serkan/embedded/gdb/break 

  Breakpoint 1, 0x000000000040058a in main ()

Bu noktada sembolik ve gerçek makina kodlarına baktığımızda, main fonksiyonunun başlangıç kodlarından (prologue) sonraki 0x40058a adresindeki ilk komutunun kesme noktası olarak gösterildiğini görüyoruz. Kesme noktasındaki makina komutunun 48 ile başladığını görmekteyiz. Programın bir önceki çalışmasında da 5. sıradaki makina komutunun 48 olarak gösterildiğine dikkat ediniz. Bu noktadan sonra continue komutu ile programı çalıştırıyoruz.

(gdb) disas /r
  Dump of assembler code for function main:
    0x0000000000400586 <+0>:     55      push   %rbp
    0x0000000000400587 <+1>:     48 89 e5        mov    %rsp,%rbp
  =>0x000000000040058a <+4>:     48 83 ec 10     sub    $0x10,%rsp
  ...

Bu sefer 5. sıradaki makina komutunun, beklediğimiz üzere, 48 yerine cc ile gösterildiğini görmekteyiz. Buna karşın, disas çıktısında bu değer hala 48 ile gösterilmeye devam etmektedir.

(gdb) c
  Continuing.
  0x400586: 55 48 89 e5 cc 83 ec 10 48 c7 45 f8 86 05 40 00 
  0x400596: c7 45 f4 00 00 00 00 eb 5a 48 8b 45 f8 48 89 c6 
  [Inferior 1 (process 22442) exited normally]

Yazılımsal kesme noktaları bazı zorluklar barındırmaktadır. Kesme noktasından sonra akış devam ettirilmek istendiğinde, 0xCC makina kodu yerine gerçek makina kodu geri yazılmalı, akış devam ettirilmeli fakat kesme noktasının hala kullanılabilir olması için hemen ardından 0xCC kodu geri yazılmalıdır. Kesme noktasında program askıya alındığında komut göstericisi bir sonraki komutu göstermektedir. Akışın devam ettirilebilmesi için komut göstericisi tekrardan kesme noktasını gösterecek şekilde ayarlanmalı ve işlemci pipeline'ı temizlenmelidir.

Donanımsal Kesme Noktaları

Donamımsal kesme noktalarıyla, yazılımsal kesme noktalarının aksine, veri belleğini de, etkin bir şekilde izlemek mümkündür. Donanımsal kesme noktaları, işlemci bünyesinde, özel gereksinimlere ihtiyaç duymaktadır. x86 mimarisinde bu amaçla, DRx şeklinde isimlendirilen, 6 adet debug yazmacı bulunmaktadır. Bu yazmaçlardan 4 tanesi adres yolunu dinlemekte, iki tanesi ise kontrol ve durum bilgisi için kullanılmaktadır. Bu yazmaçlar, aktif olmaları durumunda, kendilerine yüklenen değerleri adres yolundaki değerlerle karşılaştırmaktadırlar. Adreslerin eşleşmesi durumunda, adres üzerinde, okuma, yazma veya çalıştırma işlemleri yapılmasına göre içsel bir kesmeye neden olmaktadırlar. Bu durumda akış durmakta, araya işletim sisteminin ele alım kodu girmekte ve nihayetinde, yazılımsal kesmelerde olduğu gibi, debugger bu durumdan haberdar olmaktadır. Bu özelliğin olmaması durumunda bellek izlenmek istendiğinde debugger her makina komutundan sonra izlenen bellek bölgesiyle ilgili işlem yapılıp yapılmadığına bakmalıdır. Donanımsal kesme noktaları, yazılımsal kesme noktalarının aksine, kısıtlı sayıda olmalarına karşın, değişkenlerin izlenmek istendiği ve kodun, FLASH bellek gibi, değiştirilemediği ortamlarda kod üzerinde de kesme noktaları oluşturabilmek için oldukça faydalıdırlar. gdb'nin kod üzerindeki kesme noktalarını yazılımsal, bellek üzerindeki izleme noktalarının ise donanımsal olarak gerçekleştirdiğini görmekteyiz.

Kesme Noktalarının Silinmesi ve Etkisizleştirilmesi

Kesme noktalarını delete ve disable komutlarıyla silebilir veya etkisizleştirebilirsiniz. Kod veya bellek üzerinde oluşturduğunuz kesme noktalarını info breakpoints ile listeleyebilir, kesme noktalarının durumlarına ve numaralarına ulaşabilirsiniz. disable ve delete komutları kesme numaralarını argüman olarak almaktadır, argüman geçirilmemesi durumunda tüm kesme noktaları üzerinde işlem yapılır. Aşağıdaki örnek kullanımı inceleyiniz.

(gdb) info breakpoints 
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x080483f1 in main at debug.c:6
    breakpoint already hit 1 time
2       hw watchpoint  keep y              i
(gdb) disable 1
(gdb) info breakpoints 
Num     Type           Disp Enb Address    What
1       breakpoint     keep n   0x080483f1 in main at debug.c:6
    breakpoint already hit 1 time
2       hw watchpoint  keep y              i
(gdb) delete 2
(gdb) info breakpoints 
Num     Type           Disp Enb Address    What
1       breakpoint     keep n   0x080483f1 in main at debug.c:6
  breakpoint already hit 1 time

results matching ""

    No results matching ""