Maintenant que le bug Improve gnu ELF est corrigé, kcachegrind nous génère de beaux callgraphs.
Ce patch consistait à ajouter des instructions .size (dans l'assembleur ELF) pour que valgrind interprète tous les symboles (camT_entry, camlT_foo, camlT_bar, ...) et puisse ainsi afficher leurs noms au lieu d'un nombre hexadécimal!
Les standards ELF pour le debug
Maintenant, nous souhaiterions que ces outils puissent savoir de manière standard dans quels fichiers et à quelles lignes sont déclarés les fonctions. (Le nom du fichier est bien présent dans les symboles, mais cela ne permet pas d'exploiter le plein potentiel de ces outils)
Voyons un peu comment se débrouille GCC :
int bar(int a) { return 1+a; }int foo(int a) { return 2+bar(a); }
int main() { foo(3); }
$ gcc -O0 -g -S t.c
.globl bar .type bar, @function bar: .LFB2: .file 1 "t.c" .loc 1 1 0 pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: movl %edi, -4(%rbp) .loc 1 2 0 movl -4(%rbp), %eax incl %eax .loc 1 3 0 leave ret .LFE2: .size bar, .-bar .globl foo .type foo, @function
Les instructions clés sont .file et .loc :
- .file déclare un chemin vers un fichier et l'associe à un
identifiant
-> .file $identifiant$ "$chemin_du_fichier"
- .loc associe un fichier, un numéro de ligne et de colonne aux instructions
la succédant
-> .loc $identifiant_de_fichier$ $ligne$ $colonne$
Solution proposée
Le module du compilateur qui génère les instructions est : asmcomp/i386/emit.mlp
Et plus particulièrement la fonction fundecl :
let fundecl fundecl = function_name := fundecl.fun_name; fastcode_flag := fundecl.fun_fast; (* ... *) ` .globl {emit_symbol fundecl.fun_name} `; `{emit_symbol fundecl.fun_name}: `; if !Clflags.gprofile then emit_profile(); let n = frame_size() - 4 in if n > 0 then ` subl ${emit_int n}, %esp `; `{emit_label !tailrec_entry_point}: `; emit_all true fundecl.fun_body; List.iter emit_call_gc !call_gc_sites; emit_call_bound_errors (); List.iter emit_float_constant !float_constants; match Config.system with "linux_elf" | "bsd_elf" | "gnu" -> ` .type {emit_symbol fundecl.fun_name},@function `; ` .size {emit_symbol fundecl.fun_name},.-{emit_symbol fundecl.fun_name} ` | _ -> ()
Hors nous n'avons à disposition que la variable fundecl :
type fundecl = { fun_name: string; fun_body: instruction; fun_fast: bool } type instruction = { mutable desc: instruction_desc; mutable next: instruction; arg: Reg.t array; res: Reg.t array; dbg: Debuginfo.t; live: Reg.Set.t }
Il y a bien un attribut dbg sur les instructions mais il est rarement renseigné. (une compilation avec l'option -dlinear permet de le voir)
J'ai décidé d'ajouter un attribut fun_dbg : Debuginfo.t sur fundecl et de le remplir dans toutes les phases de compilation. Il serait peut être plus judicieux de travailler sur l'attribut dbg des instructions ? (car cela permettrait par la suite d'indiquer les lignes de chaque instruction à valgrind, mais aussi gdb!) Ce patch n'est pas optimisé car il répète l'instruction .file pour chaque .loc, donc à chaque entête de fonction, nous avons un .file et un .loc.
-> Patch sur
la branche release311
Voyons maintenant ce qu'apporte ce patch.
Résultats avec gdb
$ ocamlopt -g -inline 0 t.ml $ gdb a.out (gdb) break t.ml:6 Breakpoint 1 at 0x8049940: file t.ml, line 6. (gdb) run Starting program: /home/alex/callgraph/a.outBreakpoint 1, camlT__foo_60 () at t.ml:6 6 let foo i = Current language: auto; currently asm
(gdb) backtrace #0 camlT__foo_60 () at t.ml:7 #1 0x0804c570 in camlT__entry () at t.ml:12 #2 0x0806e4b7 in caml_start_program ()
(gdb) step 1 camlT__bar_58 () at t.ml:2 2 let bar i =
(gdb) list 1 2 let bar i = 3 1+i 4 ;; 5 6 let foo i = 7 2+(bar i ) 8 ;; 9 10 let () =
Résultats avec gprof
$ ocamlopt -g -p -inline 0 t.ml $ ./a.out $ gprof -A *** File /home/alex/callgraph/t.ml:1 -> let bar i = Thread.delay 3.0; 1+i ;; 1 -> let foo i = 2+(bar i ) ;; let () = 1 -> let closure() = 3 in print_int ( foo (closure()) ) ;;
Top 10 Lines:
Line Count 2 1 7 1 12 1
Execution Summary:
3 Executable lines in this file 3 Lines executed
100.00 Percent of the file executed
3 Total number of line executions 1.00 Average executions per line
Résultats avec valgrind/kcachegrind
$ ocamlopt -g -inline 0 t.ml $ valgrind --tool=callgrind ./a.out $ callgrind_annotate callgrind.out.2152 t.ml-- User-annotated source: t.ml
.
8 let bar i = 77,715 => thread.ml:camlThread__delay_75 (1x) . Thread.delay 3.0; . 1+i . ;; .
3 let foo i = 77,723 => t.ml:camlT__bar_58 (1x) . 2+(bar i ) . ;; .
. let () = 13 let closure() = 3 in 1,692 => pervasives.ml:camlPervasives__output_string_215 (1x) 2,312 => pervasives.ml:camlPervasives__string_of_int_154 (1x) 77,726 => t.ml:camlT__foo_60 (1x) . print_int ( foo (closure()) ) . ;; .
$ kcachegrind callgrind.out.2152
Et après ?
On peut espérer encore un tas d'améliorations, comme :
- des breakpoints sur n'importe quelle ligne de caml ...
- un pluging gdb pour pouvoir lire des valeurs pendant un break!
Comments
You can use your Fediverse (i.e. Mastodon, among many others) account to reply to this post
(Note that comments from locked accounts won't be visible on the blog, but only to me)