Petualangan dengan ptrace (2)

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:

  1. 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.
  2. 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); } /*      ptrace,    . */ /* *  ,      *  ,      . *    --  API  ptrace! */ /*       PTRACE_SYSCALL. */ } /* (argc, argv) --    ,    . */ 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 :-)

Source: https://habr.com/ru/post/id439882/


All Articles