GDB ile Hata Ayıklama (Debug 🐛)
Şimdi, GDB ile kendi programımız debug etmeye çalışalım, olacak mı bilmiyorum.
Çekirdek, Hardware Thread, hart Sayısı Ayarlama
Öncesinde bir konuya değinmek istiyorum. make qemu
dediğimiz zaman aslında
çok çekirdekli bir sanal bilgisayar oluşuyor, elbette RISC-V. Default olarak
QEMU -smp 3
diye bir argümanla çağrılıyor, 3 çekirdekli bir makine sayı
biraz ilginç. QEMU çalışırken top -c
dersek QEMU’nun %300 CPU kullandığını
gördüm. Yani muhtemelen her bir sanal RISC-V çekirdeği için bilgisayarımdaki
bir core’u harcıyor. Normalde QEMU %100 CPU kullanan bir uygulama değil. Fakat
emule ettiği sistem hiç uyumuyorsa mesela infinite loopta dönüyorsa her core
böyle olabilir belki. Her ne kadar bu kadar CPU kullanımı garip gelse de
ileriye dönük bu konuyu bırakıyorum.
xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
Buradaki hart x
core sayısı ile ilgili. Koda bir bakalım [1]:
13if(cpuid() == 0){
14 consoleinit();
15 printfinit();
16 printf("\n");
17 printf("xv6 kernel is booting\n");
18 printf("\n");
19 kinit(); // physical page allocator
20 kvminit(); // create kernel page table
21 kvminithart(); // turn on paging
22 procinit(); // process table
23 trapinit(); // trap vectors
24 trapinithart(); // install kernel trap vector
25 plicinit(); // set up interrupt controller
26 plicinithart(); // ask PLIC for device interrupts
27 binit(); // buffer cache
28 iinit(); // inode table
29 fileinit(); // file table
30 virtio_disk_init(); // emulated hard disk
31 userinit(); // first user process
32 __sync_synchronize();
33 started = 1;
34} else {
35 while(started == 0)
36 ;
37 __sync_synchronize();
38 printf("hart %d starting\n", cpuid());
39 kvminithart(); // turn on paging
40 trapinithart(); // install kernel trap vector
41 plicinithart(); // ask PLIC for device interrupts
42}
Kodu takip ederseniz cpuid()
nin ilgili çekirdeğin hart
yani hardware thread
ID’si olduğunu görebiliyoruz [2]. RISC-V mimarisi detaylarına şimdilik bakmıyoruz
ama hart’ları birer core gibi düşünebiliriz diye anlıyorum [3]
Burada ID 0 olanı niye bastırmamışlar bilmiyorum, çünkü 1 ve 2 görünce 2 çekirdek düşünebiliriz aslında 3 çalışıyor.
İstersek çekirdek sayısını değiştirebiliriz. Makefile
içerisinde CPUS
diye
bir çevresel değişken, environment variable, okunuyor, default 3. Bunu geçersek
core sayısını değiştirebiliriz.
ay@dsklin:~/ws/xv6-riscv$ CPUS=1 make qemu
xv6 kernel is booting
init: starting sh
$ QEMU: Terminated
ay@dsklin:~/ws/xv6-riscv$ CPUS=8 make qemu
xv6 kernel is booting
hart 6 starting
hart 7 starting
hart 3 starting
hart 2 starting
hart 1 starting
hart 4 starting
hart 5 starting
init: starting sh
Tek core ya da 8 core açabiliriz. Benim sistemde her core %100 CPU kullanıyor,
onu tekrar belirteyim. make CPUS=4 qemu
diyebiliriz alternatif olarak,
CPUS
u ortaya alma şeklinde.
GDB
Şimdi gelelim esas konuya. QEMU üzerinde çalıştırdığı uygulamayı, sanal makinede çalışan uygulamayı sanki JTAG gibi doğrudan donanıma bağlıymışız gibi GDB ile debug etmeye imkan sağlıyor [4]:
QEMU supports working with gdb via gdb’s remote-connection facility (the “gdbstub”). This allows you to debug guest code in the same way that you might with a low-level debug facility like JTAG on real hardware. You can stop and start the virtual machine, examine state like registers and memory, and set breakpoints and watchpoints.
Eğer xv6’yı make qemu-gdb
ile çalıştırırsak QEMU bu modda çalışıyor.
ay@dsklin:~/ws/xv6-riscv$ make qemu-gdb
sed "s/:1234/:26000/" < .gdbinit.tmpl-riscv > .gdbinit
*** Now run 'gdb' in another window.
qemu-system-riscv64 ... -S -gdb tcp::26000
Burada bizler için .gdbinit
dosyası oluşuyor ve QEMU çalışırken
S -gdb tcp::26000
parametresi geçiliyor. Şimdi başka bir pence açıp aynı
dizine gidip gdb
diyoruz. gdb
, buradaki .gdbinit
dosyasını okuyacak.
Şöyle bir hata aldık:
warning: File "/home/ay/ws/xv6-riscv/.gdbinit" auto-loading has been declined by
your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
Önce bunu düzeltelim [5].
$HOME/.config/gdb/gdbinit
i açıp
add-auto-load-safe-path /home/ay/ws/xv6-riscv/.gdbinit
ekliyoruz.
Bende bu dosya yoktu, yarattım.
Daha sonra gdb
dediğimiz zaman bu sefer bu hata geliyor fakat
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://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".
.gdbinit:2: Error in sourced command file:
Undefined item: "riscv:rv64".
gibi bir hata aldık. Ben gdb
yazdığım için RISC-V destekleyen cross GDB ?
değil, klasik gdb çalıştı. riscv64-linux-gnu-gdb
çalıştıracaktım ama
böyle bir dosya sanırım yok [6]. gdb-multiarch
ile yapalım, bu sefer oldu.
Şöyle bir uyarı aldık:
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x0000000000001000 in ?? ()
Şimdilik göz ardı ediyorum. gdb
de c
yani continue dediğimizde xv6 çalışmaya
başlıyor. Ctrl-C
ile durdurabiliyoruz. Bunu durdurunca CPU kullanımı da 0’a
düşüyor, durduğu nokta bende schedular oldu. Daha sonra c
ile devam edebiliriz.
loop.c
Debug
Şimdi bir önceki yazıda yazdığımız loop.c
kodunu debug etmeye çalışalım.
Mesela Ctrl-d
ile programdan çıkabiliyorduk. Bizim kodun çıkması için read()
geri dönüş değerinin 0 veya daha küçük olması gerekiyor. Bakalım hangi değeri
dönüyormuş.
make qemu-gdb
ile bir pencerede başlatalım xv6’yı, diğer pencerede
gdb-multiarch
çalıştıralım. c
diyelim ve xv6’yı salalım çalışsın.
xv6’da loop
diyerek yazılımı çalıştırıyoruz. Beklediğimiz gibi çalışıyor.
Şimdi gdb
de Ctrl-C
diyelim ve target’ı durduralım. gdb
de
file user/_loop
diyerek gdb
ye çalışan komutu tanıtalım. Sonra list main
diyerek kaynak kodunu listeleyelim:
(gdb) file user/_loop
Reading symbols from user/_loop...
(gdb) list main
5 static const char msg1[] = "read yapildi\n";
6 static const char msg2[] = "write yapildi\n";
7 static const char msg3[] = "read cikti!!!\n";
8
9 int main()
10 {
11 for (;;){
12 int n = read(0 , buf, sizeof(buf));
13 write(1, msg1, sizeof(msg1) - 1);
14 if (n <= 0) {
(gdb)
n
nin değerini görmek için mesela satır 13’e breakpoint koyalım, b 13
diyerek.
(gdb) b 13
Breakpoint 1 at 0x56: file user/loop.c, line 13.
Şimdi c
diyerek programımızı devam ettirelim. Mesela test
yazıp Enter diyelim.
Programımız breakpoint’te duracaktır. Şimdi p n
yazıp gdb’de n
nin değerine
bakalım. 5
miş. Neden? Çünkü test
+ Enter 5 karakter. c
deyip devam edelim.
Şimdi loop programına Ctrl-d
verelim. Yine breakpointte durduk. p n
diyoruz
ve 0 değerini gördük. Demek ki read()
bize 0 dönüyormuş bu durumda. c
dersek programımızın çıkığını göreceğiz. QEMU’dan yine Ctrl-a
x
,le çıkabiliriz.
gdb
den de quit
deyip çıkabiliriz. Böylece programımız debug edebildik, ne hoş
Bu altyapı kernelin kendsinin de debug işlemi için kullanılabilir. Onun için
file kernel/kernel
dememiz gerekecek gdb’ye. Demesek olur mu tam emin değilim,
ileride bakarız.
💭 Yorumlar
Yorum altyapısı giscus tarafından (evet tarafından!) sağlanmaktadır. Yorum yazabilmek için GitHub hesabınız üzerinden giriş yapmanız gerekmektedir. Yorumlar, Github Discussions üzerinde saklanmaktadır.
be46d181-0399-4aaa-af9e-7ff85ff34934