Présentation | Parfois, l'information la plus importante que vous devez connaître est la manière dont l'état actuel de votre programme en est arrivé là. Il existe une commande backtrace, qui vous donne la chaîne d'appels de fonction actuelle de votre programme. Cet article vous montrera comment implémenter le déroulement de la pile sur x86_64 pour générer un tel traçage. |
Ces liens seront mis en ligne au fur et à mesure que d'autres articles seront publiés.
Utilisez le programme suivant comme exemple :
void a() { //stopped here } void b() { a(); } void c() { a(); } int main() { b(); c(); }
Si le débogueur s'arrête à la ligne //stopped here', il existe deux manières d'y accéder : main->b->a ou main->c->a`. Si nous définissons un point d'arrêt avec LLDB, continuons l'exécution et demandons un traçage, alors nous obtenons ce qui suit :
* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3 frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6 frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14 frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291 frame #4: 0x0000000000400409 a.out`_start + 41
Cela signifie que nous sommes actuellement dans la fonction a, a saute de la fonction b, b saute de la fonction principale, etc. Les deux dernières images indiquent comment le compilateur amorce la fonction principale.
La question est maintenant de savoir comment l'implémenter sur x86_64. L'approche la plus robuste serait d'analyser la partie .eh_frame du fichier ELF et de comprendre comment dérouler la pile à partir de là, mais ce serait pénible. Vous pourriez le faire en utilisant libunwind ou similaire, mais c'est ennuyeux. Au lieu de cela, nous supposons que le compilateur a configuré la pile d'une manière ou d'une autre et nous la parcourrons manuellement. Pour ce faire, nous devons d’abord comprendre la disposition de la pile.
High | ... | +---------+ +24| Arg 1 | +---------+ +16| Arg 2 | +---------+ + 8| Return | +---------+ EBP+--> |Saved EBP| +---------+ - 8| Var 1 | +---------+ ESP+--> | Var 2 | +---------+ | ... | Low
Comme vous pouvez le voir, le pointeur du dernier frame de pile est stocké au début du frame de pile actuel, créant une liste chaînée de pointeurs. La pile est déroulée en fonction de cette liste chaînée. Nous pouvons trouver la fonction de la trame suivante dans la liste en recherchant l'adresse de retour dans le message DWARF. Certains compilateurs ignoreront le suivi de l'adresse de base de trame d'EBP, car celle-ci peut être exprimée sous la forme d'un décalage par rapport à ESP et libérer un registre supplémentaire. Même avec les optimisations activées, passer -fno-omit-frame-pointer à GCC ou Clang le forcera à suivre les conventions dont nous dépendons.
Nous ferons tout le travail dans la fonction print_backtrace :
void debugger::print_backtrace() {
La première chose à décider est le format à utiliser pour imprimer les informations du cadre. J'ai utilisé un lambda pour déployer cette méthode :
auto output_frame = [frame_number = 0] (auto&& func) mutable { std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func) << ' ' << dwarf::at_name(func) << std::endl; };
La première image imprimée est l’image en cours d’exécution. Nous pouvons obtenir des informations sur cette trame en recherchant le compteur de programme actuel dans DWARF :
auto current_func = get_function_from_pc(get_pc()); output_frame(current_func);
Ensuite, nous devons obtenir le pointeur de trame et l'adresse de retour de la fonction actuelle. Le pointeur de trame est stocké dans le registre rbp et l'adresse de retour est empilée sur 8 octets à partir du pointeur de trame.
auto frame_pointer = get_register_value(m_pid, reg::rbp); auto return_address = read_memory(frame_pointer+8);
Nous avons maintenant toutes les informations dont nous avons besoin pour étendre la pile. Je continue de me dérouler jusqu'à ce que le débogueur atteigne main, mais vous pouvez également choisir de vous arrêter lorsque le pointeur de trame est 0x0, qui sont les fonctions que vous appelez avant d'appeler la fonction principale. Nous récupérerons le pointeur de trame et l’adresse de retour de chaque trame et imprimerons les informations.
while (dwarf::at_name(current_func) != "main") { current_func = get_function_from_pc(return_address); output_frame(current_func); frame_pointer = read_memory(frame_pointer); return_address = read_memory(frame_pointer+8); } }
C'est tout ! Voici l'intégralité de la fonction :
void debugger::print_backtrace() { auto output_frame = [frame_number = 0] (auto&& func) mutable { std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func) << ' ' << dwarf::at_name(func) << std::endl; }; auto current_func = get_function_from_pc(get_pc()); output_frame(current_func); auto frame_pointer = get_register_value(m_pid, reg::rbp); auto return_address = read_memory(frame_pointer+8); while (dwarf::at_name(current_func) != "main") { current_func = get_function_from_pc(return_address); output_frame(current_func); frame_pointer = read_memory(frame_pointer); return_address = read_memory(frame_pointer+8); } }
Bien sûr, nous devons exposer cette commande à l'utilisateur.
else if(is_prefix(command, "backtrace")) { print_backtrace(); }
Une façon de tester cette fonctionnalité consiste à écrire un programme de test avec un tas de petites fonctions qui s'appellent les unes les autres. Définissez quelques points d'arrêt, parcourez le code et assurez-vous que votre traçage est exact.
Nous avons parcouru un long chemin depuis un programme qui ne pouvait que générer et s'attacher à d'autres programmes. L'avant-dernier article de cette série complétera l'implémentation du débogueur en prenant en charge la lecture et l'écriture de variables. En attendant, vous pouvez trouver le code de cet article ici.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!