
Pada Habré sudah
menulis tentang intersepsi panggilan sistem melalui
ptrace
; Alexa menulis tentang posting yang jauh lebih terperinci ini, yang saya putuskan untuk diterjemahkan.
Mulai dari mana
Komunikasi antara program yang di-debug dan debugger terjadi menggunakan sinyal. Ini sangat menyulitkan hal-hal yang sudah sulit; untuk bersenang-senang, Anda dapat membaca
bagian BUGS dari man ptrace
.
Setidaknya ada dua cara berbeda untuk memulai debugging:
ptrace(PTRACE_TRACEME, 0, NULL, NULL)
akan membuat induk dari proses saat ini menjadi debugger untuknya. Tidak diperlukan bantuan dari orang tua; man
dengan lembut menyarankan: "Suatu proses mungkin seharusnya tidak membuat permintaan ini jika orang tuanya tidak berharap untuk melacaknya." (Di tempat lain di mans Anda melihat frasa "mungkin tidak boleh" ?) Jika proses saat ini sudah memiliki debugger, maka panggilan akan gagal.ptrace(PTRACE_ATTACH, pid, NULL, NULL)
akan membuat proses saat ini menjadi debugger untuk pid
. Jika pid
sudah memiliki debugger, maka panggilan akan gagal. SIGSTOP
dikirim ke proses debug, dan tidak akan terus bekerja sampai debugger menghapusnya.
Kedua metode ini sepenuhnya independen; Anda dapat menggunakan salah satu atau yang lain, tetapi tidak ada gunanya menggabungkan mereka.
Penting untuk dicatat bahwa
PTRACE_ATTACH
tidak instan: setelah
ptrace(PTRACE_ATTACH)
dipanggil, biasanya
waitpid(2)
PTRACE_ATTACH
untuk menunggu sampai
PTRACE_ATTACH
"bekerja".
Anda dapat memulai proses anak di bawah debugging menggunakan
PTRACE_TRACEME
sebagai berikut:
static void tracee(int argc, char **argv) { if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) die("child: ptrace(traceme) failed: %m"); if (raise(SIGSTOP)) die("child: raise(SIGSTOP) failed: %m"); execvp(argv[0], argv); die("tracee start failed: %m"); } static void tracer(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) die("waitpid failed: %m"); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { kill(pid, SIGKILL); die("tracer: unexpected wait status: %x", status); } } void shim_ptrace(int argc, char **argv) { pid_t pid = fork(); if (pid < 0) die("couldn't fork: %m"); else if (pid == 0) tracee(argc, argv); else tracer(pid); die("should never be reached"); }
Tanpa panggilan
raise(SIGSTOP)
, mungkin ternyata
execvp(3)
akan dieksekusi sebelum proses induk siap untuk ini; dan kemudian tindakan debugger (misalnya, mencegat panggilan sistem) tidak akan mulai dari awal proses.
Ketika debugging dimulai, setiap
ptrace(PTRACE_SYSCALL, pid, NULL, NULL)
akan "mencairkan" proses debug sampai entri pertama ke dalam panggilan sistem, dan kemudian sampai panggilan sistem pergi.
Assembler telekinetik
ptrace(PTRACE_SYSCALL)
tidak mengembalikan informasi
apa pun ke debugger; ia hanya berjanji bahwa proses yang sedang di-debug akan berhenti dua kali pada setiap panggilan sistem. Untuk mendapatkan informasi tentang apa yang terjadi dengan proses debug - misalnya, yang dihentikannya oleh sistem - Anda perlu naik ke salinan register yang disimpan oleh kernel dalam
struct user
dalam format yang tergantung pada arsitektur spesifik. (Misalnya, pada x86_64, nomor panggilan akan berada di bidang
regs.orig_rax
, parameter pertama yang dilewati adalah di
regs.rdi
, dll.) Alexa berkomentar: “rasanya seperti Anda menulis kode rakitan di C yang bekerja dengan register prosesor jarak jauh.”
Alih-alih struktur yang dijelaskan dalam
sys/user.h
, mungkin lebih nyaman menggunakan konstanta indeks yang didefinisikan dalam
sys/reg.h
:
#include <sys/reg.h> /* . */ long ptrace_syscall(pid_t pid) { #ifdef __x86_64__ return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX); #else // ... #endif } /* . */ uintptr_t ptrace_argument(pid_t pid, int arg) { #ifdef __x86_64__ int reg = 0; switch (arg) { case 0: reg = RDI; break; case 1: reg = RSI; break; case 2: reg = RDX; break; case 3: reg = R10; break; case 4: reg = R8; break; case 5: reg = R9; break; } return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL); #else // ... #endif }
Dalam hal ini, dua perhentian proses debug - di pintu masuk ke panggilan sistem dan di pintu keluar dari itu - tidak berbeda dengan cara apa pun dari sudut pandang debugger; sehingga debugger itu sendiri harus mengingat keadaan masing-masing proses yang di-debug: jika ada beberapa, maka tidak ada yang menjamin bahwa sepasang sinyal dari satu proses akan muncul secara berurutan.
Keturunan
Salah satu opsi
ptrace
, yaitu
PTRACE_O_TRACECLONE
, memastikan bahwa semua anak dari proses debugged akan secara otomatis didebug ketika mereka
keluar dari fork(2)
. Poin halus tambahan di sini adalah bahwa keturunan yang diambil untuk debugging menjadi "pseudo-children" dari debugger, dan
waitpid
akan merespons tidak hanya untuk menghentikan "anak-anak langsung", tetapi juga untuk menghentikan debugging "pseudo-anak". Man memperingatkan tentang ini:
"Mengatur bendera WCONTINUED saat memanggil waitpid (2) tidak dianjurkan: keadaan" lanjutan "adalah per-proses dan mengkonsumsinya dapat membingungkan orang tua asli jejak." - yaitu "Pseudo-children" memiliki dua orang tua yang dapat menunggu mereka untuk berhenti. Untuk programmer debugger, ini berarti bahwa
waitpid(-1)
akan menunggu tidak hanya untuk anak-anak terdekat untuk berhenti, tetapi untuk semua proses yang di-debug.
Sinyal
(Konten bonus dari penerjemah: informasi ini tidak ada dalam artikel berbahasa Inggris)Seperti yang telah disebutkan di awal, komunikasi antara program yang di-debug dan debugger terjadi menggunakan sinyal. Suatu proses menerima
SIGSTOP
ketika debugger terhubung, dan kemudian
SIGTRAP
setiap kali sesuatu yang menarik terjadi dalam proses yang sedang di-debug - misalnya, panggilan sistem atau menerima sinyal eksternal. Debugger, pada gilirannya, menerima
SIGCHLD
setiap kali salah satu proses yang di-debug (tidak harus anak langsung) "membeku" atau "membeku".
ptrace(PTRACE_SYSCALL)
proses debugged dilakukan dengan memanggil
ptrace(PTRACE_SYSCALL)
(sebelum sinyal pertama atau system call) atau
ptrace(PTRACE_CONT)
(sebelum sinyal pertama). Ketika sinyal
SIGSTOP/SIGCONT
juga digunakan untuk tujuan yang tidak terkait dengan debugging, masalah dengan
ptrace
dapat muncul: jika debugger "tidak meringkas" proses debugged yang menerima
SIGSTOP
, maka dari luar akan terlihat seolah-olah sinyal diabaikan; jika debugger tidak "mencairkan" proses yang sedang di-debug, maka
SIGCONT
eksternal tidak bisa "mencairkannya".
Sekarang bagian yang menyenangkan: Linux melarang proses
debugging sendiri , tetapi tidak mencegah penciptaan loop ketika orang tua dan anak saling men-debug. Dalam hal ini, ketika salah satu proses menerima sinyal eksternal, itu "membeku" melalui
SIGTRAP
- maka
SIGCHLD
dikirim ke proses kedua, dan juga "membeku" melalui
SIGTRAP
. Tidak mungkin untuk mendapatkan "co-debuggers" seperti itu dari jalan buntu dengan mengirimkan
SIGCONT
dari luar; satu-satunya cara adalah membunuh (
SIGKILL
) anak, maka orang tua akan keluar dari debugging dan "membekukan". (Jika Anda membunuh orang tua, anak itu akan mati bersamanya.) Jika anak itu mengaktifkan opsi
PTRACE_O_EXITKILL
, maka orang tua yang
PTRACE_O_EXITKILL
akan mati bersama kematiannya.
Sekarang Anda tahu bagaimana menerapkan sepasang proses yang, ketika menerima sinyal apa pun, keduanya membeku selamanya dan mati hanya bersama. Mengapa ini mungkin perlu dalam praktek, saya tidak akan menjelaskan :-)