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 <puts@plt>
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ĞER@N | İ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 i@5
$10 = {111, 0, 1, 2, 3}
(gdb) print /x i@5
$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