GNU/Linux >> Belajar Linux >  >> Linux

Tutorial praktis untuk menggunakan GNU Project Debugger

Jika Anda seorang programmer dan ingin menempatkan fungsionalitas tertentu dalam perangkat lunak Anda, Anda mulai dengan memikirkan cara untuk mengimplementasikannya—seperti menulis metode, mendefinisikan kelas, atau membuat tipe data baru. Kemudian Anda menulis implementasi dalam bahasa yang dapat dipahami oleh kompiler atau juru bahasa. Tetapi bagaimana jika kompiler atau juru bahasa tidak memahami instruksi seperti yang Anda pikirkan, meskipun Anda yakin Anda melakukan semuanya dengan benar? Bagaimana jika sebagian besar perangkat lunak berfungsi dengan baik tetapi menyebabkan bug dalam keadaan tertentu? Dalam kasus ini, Anda harus tahu cara menggunakan debugger dengan benar untuk menemukan sumber masalah Anda.

GNU Project Debugger (GDB) adalah alat yang ampuh untuk menemukan bug dalam program. Ini membantu Anda menemukan alasan kesalahan atau kerusakan dengan melacak apa yang terjadi di dalam program selama eksekusi.

Artikel ini adalah tutorial praktis tentang penggunaan GDB dasar. Untuk mengikuti contoh, buka baris perintah dan klon repositori ini:

git clone https://github.com/hANSIc99/core_dump_example.git

Pintasan

Lebih banyak sumber daya Linux

  • Lembar contekan perintah Linux
  • Lembar contekan perintah Linux tingkat lanjut
  • Kursus online gratis:Ikhtisar Teknis RHEL
  • Lembar contekan jaringan Linux
  • Lembar contekan SELinux
  • Lembar contekan perintah umum Linux
  • Apa itu container Linux?
  • Artikel Linux terbaru kami

Setiap perintah di GDB dapat dipersingkat. Misalnya, info break , yang menunjukkan breakpoint yang disetel, dapat disingkat menjadi i break . Anda mungkin melihat singkatan tersebut di tempat lain, tetapi dalam artikel ini, saya akan menuliskan seluruh perintahnya sehingga jelas fungsi mana yang digunakan.

Parameter baris perintah

Anda dapat melampirkan GDB ke setiap file yang dapat dieksekusi. Arahkan ke repositori yang Anda kloning, dan kompilasi dengan menjalankan make . Anda sekarang harus memiliki file yang dapat dieksekusi bernama coredump . (Lihat artikel saya tentang Membuat dan men-debug file dump Linux untuk informasi lebih lanjut..

Untuk melampirkan GDB ke executable, ketik:gdb coredump .

Output Anda akan terlihat seperti ini:

Dikatakan tidak ada simbol debug yang ditemukan.

Informasi debug adalah bagian dari file objek (yang dapat dieksekusi) dan mencakup tipe data, tanda tangan fungsi, dan hubungan antara kode sumber dan opcode. Pada titik ini, Anda memiliki dua opsi:

  • Lanjutkan men-debug rakitan (lihat "Men-debug tanpa simbol" di bawah)
  • Kompilasi dengan informasi debug menggunakan informasi di bagian berikutnya

Kompilasi dengan informasi debug

Untuk memasukkan informasi debug dalam file biner, Anda harus mengkompilasi ulang. Buka Makefile dan hapus hashtag (# ) dari baris 9:

CFLAGS =-Wall -Werror -std=c++11 -g

g option memberitahu compiler untuk memasukkan informasi debug. Jalankan make clean diikuti oleh make dan panggil GDB lagi. Anda harus mendapatkan output ini dan dapat mulai men-debug kode:

Informasi debugging tambahan akan meningkatkan ukuran executable. Dalam hal ini, ini meningkatkan executable sebanyak 2,5 kali (dari 26.088 byte menjadi 65.480 byte).

Mulai program dengan -c1 beralih dengan mengetik run -c1 . Program akan mulai dan macet saat mencapai State_4 :

Anda dapat mengambil informasi tambahan tentang program. Perintah info source memberikan informasi tentang file saat ini:

  • 101 baris
  • Bahasa:C++
  • Penyusun (versi, penyetelan, arsitektur, tanda debug, standar bahasa)
  • Format debug:DWARF 2
  • Tidak ada informasi makro praprosesor yang tersedia (bila dikompilasi dengan GCC, makro hanya tersedia bila dikompilasi dengan -g3 bendera).

Perintah info shared mencetak daftar pustaka dinamis dengan alamatnya di ruang alamat virtual yang dimuat saat startup sehingga program akan dijalankan:

Jika Anda ingin mempelajari tentang penanganan pustaka di Linux, lihat artikel saya Cara menangani pustaka dinamis dan statis di Linux .

Debug program

Anda mungkin telah memperhatikan bahwa Anda dapat memulai program di dalam GDB dengan run memerintah. run command menerima argumen baris perintah seperti yang akan Anda gunakan untuk memulai program dari konsol. -c1 switch akan menyebabkan program macet pada tahap 4. Untuk menjalankan program dari awal, Anda tidak harus keluar dari GDB; cukup gunakan run perintah lagi. Tanpa -c1 switch, program mengeksekusi loop tak terbatas. Anda harus menghentikannya dengan Ctrl+C .

Anda juga dapat menjalankan program langkah demi langkah. Dalam C/C++, titik masuknya adalah main fungsi. Gunakan perintah list main untuk membuka bagian dari kode sumber yang menunjukkan main fungsi:

main fungsi ada di baris 33, jadi tambahkan breakpoint di sana dengan mengetik break 33 :

Jalankan program dengan mengetikkan run . Seperti yang diharapkan, program berhenti di main fungsi. Ketik layout src untuk menampilkan kode sumber secara paralel:

Anda sekarang berada dalam mode antarmuka pengguna teks (TUI) GDB. Gunakan tombol panah Atas dan Bawah untuk menggulir kode sumber.

GDB menyoroti baris yang akan dieksekusi. Dengan mengetik next (n), Anda dapat menjalankan perintah baris demi baris. GBD menjalankan perintah terakhir jika Anda tidak menentukan yang baru. Untuk menelusuri kode, cukup tekan Enter kunci.

Dari waktu ke waktu, Anda akan melihat bahwa output TUI sedikit rusak:

Jika ini terjadi, tekan Ctrl+L untuk menyetel ulang layar.

Gunakan Ctrl+X+A untuk masuk dan keluar dari mode TUI sesuka hati. Anda dapat menemukan ikatan kunci lainnya di manual.

Untuk keluar dari GDB, cukup ketik quit .

Watchpoints

Inti dari program contoh ini terdiri dari mesin negara yang berjalan dalam loop tak terbatas. Variabel n_state adalah enum sederhana yang menentukan status saat ini:

while(true){
        switch(n_state){
        case State_1:
                std::cout << "State_1 reached" << std::flush;
                n_state = State_2;
                break;
        case State_2:
                std::cout << "State_2 reached" << std::flush;
                n_state = State_3;
                break;
       
        (.....)
       
        }
}

Anda ingin menghentikan program saat n_state diatur ke nilai State_5 . Untuk melakukannya, hentikan program di main berfungsi dan mengatur titik pengawasan untuk n_state :

watch n_state == State_5

Menyetel watchpoint dengan nama variabel hanya berfungsi jika variabel yang diinginkan tersedia dalam konteks saat ini.

Saat Anda melanjutkan eksekusi program dengan mengetikkan continue , Anda akan mendapatkan output seperti:

Jika Anda melanjutkan eksekusi, GDB akan berhenti ketika ekspresi watchpoint bernilai false :

Anda dapat menentukan titik pengawasan untuk perubahan nilai umum, nilai khusus, dan akses baca atau tulis.

Mengubah breakpoint dan watchpoint

Ketik info watchpoints untuk mencetak daftar titik pengawasan yang telah ditetapkan sebelumnya:

Hapus breakpoint dan watchpoint

Seperti yang Anda lihat, titik pengawasan adalah angka. Untuk menghapus watchpoint tertentu, ketik delete diikuti dengan nomor pos jaga. Misalnya, watchpoint saya memiliki nomor 2; untuk menghapus watchpoint ini, masukkan delete 2 .

Perhatian: Jika Anda menggunakan delete tanpa menentukan nomor, semua watchpoint dan breakpoint akan dihapus.

Hal yang sama berlaku untuk breakpoint. Pada tangkapan layar di bawah, saya menambahkan beberapa breakpoint dan mencetak daftarnya dengan mengetikkan info breakpoint :

Untuk menghapus satu breakpoint, ketik delete diikuti dengan nomornya. Atau, Anda dapat menghapus breakpoint dengan menentukan nomor barisnya. Misalnya, perintah clear 78 akan menghapus breakpoint nomor 7, yang diatur pada baris 78.

Menonaktifkan atau mengaktifkan breakpoint dan watchpoint

Alih-alih menghapus breakpoint atau watchpoint, Anda dapat menonaktifkannya dengan mengetikkan disable diikuti dengan nomornya. Berikut ini, breakpoints 3 dan 4 dinonaktifkan dan ditandai dengan tanda minus di jendela kode:

Anda juga dapat mengubah rentang breakpoint atau watchpoint dengan mengetikkan sesuatu seperti disable 2 - 4 . Jika Anda ingin mengaktifkan kembali poin, ketik enable diikuti dengan nomor mereka.

Titik putus bersyarat

Pertama, hapus semua breakpoint dan watchpoint dengan mengetik delete . Anda masih ingin program berhenti di main fungsi, tetapi alih-alih menentukan nomor baris, tambahkan titik henti sementara dengan menamai fungsi secara langsung. Ketik break main untuk menambahkan breakpoint di main fungsi.

Ketik run untuk memulai eksekusi dari awal, dan program akan berhenti di main fungsi.

main fungsi menyertakan variabel n_state_3_count , yang bertambah saat mesin status mencapai status 3.

Untuk menambahkan breakpoint bersyarat berdasarkan nilai n_state_3_count ketik:

break 54 if n_state_3_count == 3

Lanjutkan eksekusi. Program akan mengeksekusi mesin state tiga kali sebelum berhenti di baris 54. Untuk memeriksa nilai n_state_3_count , ketik:

print n_state_3_count

Jadikan breakpoint bersyarat

Dimungkinkan juga untuk membuat breakpoint yang ada bersyarat. Hapus breakpoint yang baru saja ditambahkan dengan clear 54 , dan tambahkan breakpoint sederhana dengan mengetikkan break 54 . Anda dapat membuat breakpoint ini bersyarat dengan mengetik:

condition 3 n_state_3_count == 9

3 mengacu pada nomor breakpoint.

Setel breakpoint di file sumber lain

Jika Anda memiliki program yang terdiri dari beberapa file sumber, Anda dapat menyetel titik henti sementara dengan menetapkan nama file sebelum nomor baris, mis., break main.cpp:54 .

Catchpoints

Selain breakpoint dan watchpoint, Anda juga dapat mengatur catchpoint. Catchpoints berlaku untuk acara program seperti melakukan syscalls, memuat pustaka bersama, atau memunculkan pengecualian.

Untuk menangkap write syscall, yang digunakan untuk menulis ke STDOUT, masukkan:

catch syscall write

Setiap kali program menulis ke keluaran konsol, GDB akan menginterupsi eksekusi.

Dalam manual, Anda dapat menemukan seluruh bab yang mencakup break-, watch-, dan catchpoints.

Mengevaluasi dan memanipulasi simbol

Pencetakan nilai variabel dilakukan dengan print memerintah. Sintaks umumnya adalah print <expression> <value> . Nilai suatu variabel dapat dimodifikasi dengan mengetikkan:

set variable <variable-name> <new-value>.

Pada tangkapan layar di bawah, saya memberikan variabel n_state_3_count nilai 123 .

/x ekspresi mencetak nilai dalam heksadesimal; dengan & operator, Anda dapat mencetak alamat dalam ruang alamat virtual.

Jika Anda tidak yakin dengan tipe data simbol tertentu, Anda dapat menemukannya dengan whatis :

Jika Anda ingin membuat daftar semua variabel yang tersedia dalam lingkup main fungsi, ketik info scope main :

DW_OP_fbreg nilai mengacu pada offset tumpukan berdasarkan subrutin saat ini.

Atau, jika Anda sudah berada di dalam suatu fungsi dan ingin membuat daftar semua variabel pada bingkai tumpukan saat ini, Anda dapat menggunakan info locals :

Periksa manual untuk mempelajari lebih lanjut tentang memeriksa simbol.

Lampirkan ke proses yang sedang berjalan

Perintah gdb attach <process-id> memungkinkan Anda untuk melampirkan ke proses yang sudah berjalan dengan menentukan ID proses (PID). Untungnya, coredump program mencetak PID saat ini ke layar, jadi Anda tidak perlu menemukannya secara manual dengan ps atau top.

Mulai sebuah instance dari aplikasi coredump:

./coredump

Sistem operasi memberikan PID 2849 . Buka jendela konsol terpisah, pindah ke direktori sumber aplikasi coredump, dan lampirkan GDB:

gdb attach 2849

GDB segera menghentikan eksekusi saat Anda melampirkannya. Ketik layout src dan backtrace untuk memeriksa tumpukan panggilan:

Output menunjukkan proses yang terputus saat menjalankan std::this_thread::sleep_for<...>(...) fungsi yang dipanggil pada baris 92 dari main.cpp .

Segera setelah Anda keluar dari GDB, proses akan terus berjalan.

Anda dapat menemukan informasi lebih lanjut tentang melampirkan ke proses yang sedang berjalan di manual GDB.

Berpindah melalui tumpukan

Kembali ke program dengan menggunakan up dua kali untuk naik dalam tumpukan ke main.cpp :

Biasanya, kompiler akan membuat subrutin untuk setiap fungsi atau metode. Setiap subrutin memiliki bingkai tumpukannya sendiri, jadi bergerak ke atas dalam bingkai tumpukan berarti bergerak ke atas di tumpukan panggilan.

Anda dapat mengetahui lebih lanjut tentang evaluasi tumpukan di manual.

Tentukan file sumber

Saat melampirkan ke proses yang sudah berjalan, GDB akan mencari file sumber di direktori kerja saat ini. Atau, Anda dapat menentukan direktori sumber secara manual dengan directory perintah.

Evaluasi file dump

Baca Membuat dan men-debug file dump Linux untuk informasi tentang topik ini.

TL;DR:

  1. Saya berasumsi Anda menggunakan Fedora versi terbaru
  2. Aktifkan coredump dengan sakelar c1:coredump -c1

  3. Muat file dump terbaru dengan GDB:coredumpctl debug
  4. Buka mode TUI dan masukkan layout src

Keluaran dari backtrace menunjukkan bahwa crash terjadi lima tumpukan frame dari main.cpp . Enter untuk melompat langsung ke baris kode yang salah di main.cpp :

Melihat kode sumber menunjukkan bahwa program mencoba membebaskan pointer yang tidak dikembalikan oleh fungsi manajemen memori. Ini menghasilkan perilaku yang tidak terdefinisi dan menyebabkan SIGABRT .

Debug tanpa simbol

Jika tidak ada sumber yang tersedia, segalanya menjadi sangat sulit. Saya memiliki pengalaman pertama saya dengan ini ketika mencoba memecahkan tantangan rekayasa balik. Hal ini juga berguna untuk memiliki beberapa pengetahuan tentang bahasa assembly.

Lihat cara kerjanya dengan contoh ini.

Buka direktori sumber, buka Makefile , dan edit baris 9 seperti ini:

CFLAGS =-Wall -Werror -std=c++11 #-g

Untuk mengkompilasi ulang program, jalankan make clean diikuti oleh make dan mulai GDB. Program tidak lagi memiliki simbol debug untuk memimpin melalui kode sumber.

Perintah info file mengungkapkan area memori dan titik masuk biner:

Titik masuk sesuai dengan awal .text area, yang berisi opcode yang sebenarnya. Untuk menambahkan breakpoint pada entry point, ketik break *0x401110 kemudian mulai eksekusi dengan mengetikkan run :

Untuk menyiapkan breakpoint di alamat tertentu, tentukan dengan operator dereferensi * .

Pilih rasa disassembler

Sebelum menggali lebih dalam perakitan, Anda dapat memilih rasa perakitan mana yang akan digunakan. Default GDB adalah AT&T, tetapi saya lebih suka sintaks Intel. Ubah dengan:

set disassembly-flavor intel

Sekarang buka perakitan dan daftarkan jendela dengan mengetik layout asm dan layout reg . Anda sekarang akan melihat output seperti ini:

Simpan file konfigurasi

Meskipun Anda telah memasukkan banyak perintah, Anda belum benar-benar memulai debugging. Jika Anda sedang men-debug aplikasi secara berat atau mencoba memecahkan tantangan rekayasa balik, menyimpan setelan khusus GDB dalam file dapat berguna.

File konfigurasi gdbinit dalam repositori GitHub proyek ini berisi perintah yang baru saja digunakan:

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

set write on perintah memungkinkan Anda untuk memodifikasi biner selama eksekusi.

Keluar dari GDB dan buka kembali dengan file konfigurasi: gdb -x gdbinit coredump .

Baca instruksi

Dengan c2 sakelar diterapkan, program akan macet. Program berhenti pada fungsi entri, jadi Anda harus menulis continue untuk melanjutkan eksekusi:

idiv instruksi melakukan pembagian bilangan bulat dengan dividen dalam RAX register dan pembagi ditentukan sebagai argumen. Hasil bagi dimuat ke dalam RAX mendaftar, dan sisanya dimuat ke RDX .

Dari tinjauan umum pendaftaran, Anda dapat melihat RAX berisi 5 , jadi Anda harus mencari tahu nilai mana yang disimpan di tumpukan pada posisi RBP-0x4 .

Membaca memori

Untuk membaca konten memori mentah, Anda harus menentukan beberapa parameter lebih banyak daripada untuk membaca simbol. Saat Anda menggulir sedikit ke atas pada keluaran perakitan, Anda dapat melihat pembagian tumpukan:

Anda paling tertarik dengan nilai rbp-0x4 karena ini adalah posisi dimana argumen untuk idiv tersimpan. Dari tangkapan layar, Anda dapat melihat bahwa variabel berikutnya terletak di rbp-0x8 , jadi variabel di rbp-0x4 lebarnya 4 byte.

Di GDB, Anda dapat menggunakan x perintah untuk memeriksa konten memori apa pun:

x/ n f u> addr>

Parameter opsional:

  • n :Hitungan berulang (default:1) mengacu pada ukuran unit
  • f :Penentu format, seperti di printf
  • u :Ukuran satuan
    • b :byte
    • h :setengah kata (2 byte)
    • w :kata (4 byte)(default)
    • g :kata raksasa (8 byte)

Untuk mencetak nilai di rbp-0x4 , ketik x/u $rbp-4 :

Jika Anda mengingat pola ini, sangat mudah untuk memeriksa memori. Periksa bagian memori pemeriksaan di manual.

Manipulasi perakitan

Pengecualian aritmatika terjadi di subrutin zeroDivide() . Saat Anda menggulir sedikit ke atas dengan tombol panah Atas, Anda dapat menemukan pola ini:

0x401211 <_Z10zeroDividev>              push   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp  

Ini disebut prolog fungsi:

  1. Penunjuk dasar (rbp ) dari fungsi panggilan disimpan di tumpukan
  2. Nilai penunjuk tumpukan (rsp ) dimuat ke penunjuk dasar (rbp )

Lewati subrutin ini sepenuhnya. Anda dapat memeriksa tumpukan panggilan dengan backtrace . Anda hanya satu tumpukan bingkai di depan main fungsi, sehingga Anda dapat kembali ke main dengan satu up :

Di main . Anda fungsi, Anda dapat menemukan pola ini:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

Subrutin zeroDivide() dimasukkan hanya jika jump equal (je) mengevaluasi ke true . Anda dapat dengan mudah menggantinya dengan jump-not-equal (jne) instruksi, yang memiliki opcode 0x75 (asalkan Anda menggunakan arsitektur x86/64; opcode berbeda pada arsitektur lain). Mulai ulang program dengan mengetikkan run . Ketika program berhenti pada fungsi entri, manipulasi opcode dengan mengetik:

set *(unsigned char*)0x401435 = 0x75

Terakhir, ketik continue . Program akan melewati subrutin zeroDivide() dan tidak akan mogok lagi.

Kesimpulan

Anda dapat menemukan GDB bekerja di latar belakang di banyak lingkungan pengembangan terintegrasi (IDE), termasuk Qt Creator dan ekstensi Native Debug untuk VSCodium.

Sangat berguna untuk mengetahui cara memanfaatkan fungsionalitas GDB. Biasanya, tidak semua fungsi GDB dapat digunakan dari IDE, jadi Anda mendapat manfaat dari pengalaman menggunakan GDB dari baris perintah.


Linux
  1. 7 trik praktis untuk menggunakan perintah wget Linux

  2. Kiat Linux untuk menggunakan Layar GNU

  3. 8 tips untuk baris perintah Linux

  1. Memecahkan masalah menggunakan sistem file proc di Linux

  2. 5 tips untuk GNU Debugger

  3. Kali di Subsistem Windows untuk Linux

  1. Kiat untuk menggunakan perintah teratas di Linux

  2. Menggunakan Perintah gratis Linux

  3. Tutorial Perintah Linux ln untuk Pemula (5 Contoh)