Skip to content

Commit 4853be0

Browse files
committed
Add addr2line clone for mach-o binaries.
1 parent 9ec1c12 commit 4853be0

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

example/addr2line.ml

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,297 @@
44
associated with that address, as well as the inline call stack leading
55
to that address.
66
*)
7+
open Durin
8+
9+
(* Helper function to resolve dSYM paths similar to other examples *)
10+
let resolve_binary_path filename =
11+
if Sys.file_exists filename then (filename, false)
12+
else
13+
let dsym_path =
14+
filename ^ ".dSYM/Contents/Resources/DWARF/" ^ Filename.basename filename
15+
in
16+
if Sys.file_exists dsym_path then (dsym_path, true) else (filename, false)
17+
18+
(* Initialize DWARF context from file *)
19+
let init_context filename =
20+
let actual_filename, _ = resolve_binary_path filename in
21+
if Sys.is_directory actual_filename then
22+
failwith (Printf.sprintf "'%s' is a directory" actual_filename)
23+
else
24+
let buffer = Object.Buffer.parse actual_filename in
25+
(buffer, actual_filename)
26+
27+
(* Find line table entry for a given address using binary search *)
28+
let find_line_entry entries target_addr =
29+
let rec binary_search low high =
30+
if low > high then None
31+
else
32+
let mid = (low + high) / 2 in
33+
let entry = List.nth entries mid in
34+
let addr = entry.Dwarf.LineTable.address in
35+
if Unsigned.UInt64.equal addr target_addr then Some entry
36+
else if Unsigned.UInt64.compare target_addr addr < 0 then
37+
binary_search low (mid - 1)
38+
else
39+
(* Check if target is between this entry and the next *)
40+
if mid < List.length entries - 1 then
41+
let next_entry = List.nth entries (mid + 1) in
42+
let next_addr = next_entry.Dwarf.LineTable.address in
43+
if Unsigned.UInt64.compare target_addr next_addr < 0 then Some entry
44+
else binary_search (mid + 1) high
45+
else Some entry
46+
(* Last entry *)
47+
in
48+
if List.length entries = 0 then None
49+
else binary_search 0 (List.length entries - 1)
50+
51+
(* Get section offset helper *)
52+
let get_section_offset buffer section_type =
53+
let object_format = Object_format.detect_format buffer in
54+
let section_name =
55+
Dwarf.object_format_to_section_name object_format section_type
56+
in
57+
try
58+
let open Macho in
59+
let _header, commands = read buffer in
60+
let sections = ref [] in
61+
List.iter
62+
(fun cmd ->
63+
match cmd with
64+
| LC_SEGMENT_64 (lazy segment) ->
65+
Array.iter
66+
(fun section ->
67+
if
68+
String.equal section.sec_segname "__DWARF"
69+
&& String.equal section.sec_sectname section_name
70+
then
71+
sections :=
72+
( Unsigned.UInt32.to_int section.sec_offset,
73+
Unsigned.UInt64.to_int section.sec_size )
74+
:: !sections)
75+
segment.seg_sections
76+
| LC_SEGMENT_32 (lazy segment) ->
77+
Array.iter
78+
(fun section ->
79+
if
80+
String.equal section.sec_segname "__DWARF"
81+
&& String.equal section.sec_sectname section_name
82+
then
83+
sections :=
84+
( Unsigned.UInt32.to_int section.sec_offset,
85+
Unsigned.UInt64.to_int section.sec_size )
86+
:: !sections)
87+
segment.seg_sections
88+
| _ -> ())
89+
commands;
90+
match !sections with
91+
| (offset, size) :: _ ->
92+
Some (Unsigned.UInt64.of_int offset, Unsigned.UInt64.of_int size)
93+
| [] -> None
94+
with _ -> None
95+
96+
(* Parse line table from debug_line section *)
97+
let parse_line_table buffer =
98+
match get_section_offset buffer Dwarf.Debug_line with
99+
| None -> None
100+
| Some (offset, _size) ->
101+
let cursor =
102+
Object.Buffer.cursor buffer ~at:(Unsigned.UInt64.to_int offset)
103+
in
104+
let header = Dwarf.LineTable.parse_line_program_header cursor buffer in
105+
let entries = Dwarf.LineTable.parse_line_program cursor header in
106+
Some (header, entries)
107+
108+
(* Resolve address to source location *)
109+
let addr_to_location _buffer header entries addr =
110+
match find_line_entry entries addr with
111+
| None -> ("??", 0)
112+
| Some entry ->
113+
let file_index = Unsigned.UInt32.to_int entry.Dwarf.LineTable.file_index in
114+
if file_index < Array.length header.Dwarf.LineTable.file_names then
115+
let file_entry = header.Dwarf.LineTable.file_names.(file_index) in
116+
let filename =
117+
if file_entry.directory = "" then file_entry.name
118+
else file_entry.directory ^ "/" ^ file_entry.name
119+
in
120+
let line = Unsigned.UInt32.to_int entry.Dwarf.LineTable.line in
121+
(filename, line)
122+
else ("??", 0)
123+
124+
(* Resolve DIE address attribute considering addr_base *)
125+
let resolve_die_address buffer addr_base addr_value =
126+
match addr_base with
127+
| Some base ->
128+
let index = Unsigned.UInt64.to_int addr_value in
129+
Dwarf.resolve_address_index buffer index base
130+
| None -> addr_value
131+
132+
(* Find function name for address by searching debug_info DIEs *)
133+
let find_function_name buffer addr =
134+
try
135+
let dwarf = Dwarf.create buffer in
136+
let compile_units = Dwarf.parse_compile_units dwarf in
137+
let rec search_cu cu_seq =
138+
match cu_seq () with
139+
| Seq.Nil -> None
140+
| Seq.Cons (unit, rest) ->
141+
let header = Dwarf.CompileUnit.header unit in
142+
let abbrev_offset =
143+
Unsigned.UInt64.of_uint32 header.debug_abbrev_offset
144+
in
145+
let _, abbrev_table = Dwarf.get_abbrev_table dwarf abbrev_offset in
146+
(match Dwarf.CompileUnit.root_die unit abbrev_table buffer with
147+
| None -> search_cu rest
148+
| Some root_die ->
149+
(* Get addr_base from root DIE if present *)
150+
let addr_base =
151+
match Dwarf.DIE.find_attribute root_die Dwarf.DW_AT_addr_base with
152+
| Some (Dwarf.DIE.UData base) -> Some base
153+
| _ -> None
154+
in
155+
let rec search_die die =
156+
(* Check if this DIE is a subprogram containing the address *)
157+
(match die.Dwarf.DIE.tag with
158+
| Dwarf.DW_TAG_subprogram | Dwarf.DW_TAG_inlined_subroutine -> (
159+
(* Get low_pc and high_pc *)
160+
let low_pc_opt =
161+
Dwarf.DIE.find_attribute die Dwarf.DW_AT_low_pc
162+
in
163+
let high_pc_opt =
164+
Dwarf.DIE.find_attribute die Dwarf.DW_AT_high_pc
165+
in
166+
match (low_pc_opt, high_pc_opt) with
167+
| Some (Dwarf.DIE.Address low_pc_raw), Some (Dwarf.DIE.Address high_pc_raw) ->
168+
(* Both are addresses - resolve them *)
169+
let low_pc = resolve_die_address buffer addr_base low_pc_raw in
170+
let high_pc = resolve_die_address buffer addr_base high_pc_raw in
171+
if
172+
Unsigned.UInt64.compare addr low_pc >= 0
173+
&& Unsigned.UInt64.compare addr high_pc < 0
174+
then
175+
match Dwarf.DIE.find_attribute die Dwarf.DW_AT_name with
176+
| Some (Dwarf.DIE.String name) -> Some name
177+
| _ -> None
178+
else None
179+
| Some (Dwarf.DIE.Address low_pc_raw), Some (Dwarf.DIE.UData offset) ->
180+
(* high_pc is offset from low_pc *)
181+
let low_pc = resolve_die_address buffer addr_base low_pc_raw in
182+
let high_pc = Unsigned.UInt64.add low_pc offset in
183+
if
184+
Unsigned.UInt64.compare addr low_pc >= 0
185+
&& Unsigned.UInt64.compare addr high_pc < 0
186+
then
187+
match Dwarf.DIE.find_attribute die Dwarf.DW_AT_name with
188+
| Some (Dwarf.DIE.String name) -> Some name
189+
| _ -> None
190+
else None
191+
| _ -> None)
192+
| _ -> None)
193+
|> function
194+
| Some name -> Some name
195+
| None ->
196+
(* Search children *)
197+
let rec search_children children_seq =
198+
match children_seq () with
199+
| Seq.Nil -> None
200+
| Seq.Cons (child, rest) -> (
201+
match search_die child with
202+
| Some name -> Some name
203+
| None -> search_children rest)
204+
in
205+
search_children die.Dwarf.DIE.children
206+
in
207+
(match search_die root_die with
208+
| Some name -> Some name
209+
| None -> search_cu rest))
210+
in
211+
search_cu compile_units
212+
with _ -> None
213+
214+
(* Main addr2line lookup function *)
215+
let lookup_address buffer addr_str show_functions =
216+
try
217+
let addr = Unsigned.UInt64.of_string addr_str in
218+
match parse_line_table buffer with
219+
| None -> if show_functions then Printf.printf "??\n??:0\n" else Printf.printf "??:0\n"
220+
| Some (header, entries) ->
221+
let filename, line = addr_to_location buffer header entries addr in
222+
if show_functions then (
223+
let func_name =
224+
match find_function_name buffer addr with
225+
| Some name -> name
226+
| None -> "??"
227+
in
228+
Printf.printf "%s\n%s:%d\n" func_name filename line)
229+
else Printf.printf "%s:%d\n" filename line
230+
with _ -> if show_functions then Printf.printf "??\n??:0\n" else Printf.printf "??:0\n"
231+
232+
(* Command-line interface *)
233+
let executable_file =
234+
let doc = "Executable file to analyze" in
235+
Cmdliner.Arg.(value & opt (some string) None & info [ "e"; "exe" ] ~docv:"FILE" ~doc)
236+
237+
let show_functions =
238+
let doc = "Show function names" in
239+
Cmdliner.Arg.(value & flag & info [ "f"; "functions" ] ~doc)
240+
241+
let show_inlines =
242+
let doc = "Unwind inlined functions" in
243+
Cmdliner.Arg.(value & flag & info [ "i"; "inlines" ] ~doc)
244+
245+
let pretty_print =
246+
let doc = "Make the output easier to read for humans" in
247+
Cmdliner.Arg.(value & flag & info [ "p"; "pretty-print" ] ~doc)
248+
249+
let basenames =
250+
let doc = "Strip directory names" in
251+
Cmdliner.Arg.(value & flag & info [ "s"; "basenames" ] ~doc)
252+
253+
let addresses =
254+
let doc = "Show addresses" in
255+
Cmdliner.Arg.(value & flag & info [ "a"; "addresses" ] ~doc)
256+
257+
let demangle =
258+
let doc = "Demangle function names" in
259+
Cmdliner.Arg.(value & flag & info [ "C"; "demangle" ] ~doc)
260+
261+
let addr_list =
262+
let doc = "Addresses to look up" in
263+
Cmdliner.Arg.(value & pos_all string [] & info [] ~docv:"ADDRESS" ~doc)
264+
265+
let addr2line_cmd exec_file show_funcs _inlines _pretty _base _addrs _dem addrs =
266+
let filename = match exec_file with Some f -> f | None -> "a.out" in
267+
try
268+
let buffer, _ = init_context filename in
269+
if List.length addrs = 0 then
270+
(* Read from stdin *)
271+
try
272+
while true do
273+
let line = input_line stdin in
274+
let addr = String.trim line in
275+
if addr <> "" then lookup_address buffer addr show_funcs
276+
done
277+
with End_of_file -> ()
278+
else
279+
(* Process command-line addresses *)
280+
List.iter (fun addr -> lookup_address buffer addr show_funcs) addrs
281+
with
282+
| Sys_error msg ->
283+
Printf.eprintf "Error: %s\n" msg;
284+
exit 1
285+
| Failure msg ->
286+
Printf.eprintf "Error: %s\n" msg;
287+
exit 1
288+
| exn ->
289+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
290+
exit 1
291+
292+
let cmd =
293+
let doc = "Convert addresses to line number/file name pairs" in
294+
let info = Cmdliner.Cmd.info "addr2line" ~doc in
295+
Cmdliner.Cmd.v info
296+
Cmdliner.Term.(
297+
const addr2line_cmd $ executable_file $ show_functions $ show_inlines
298+
$ pretty_print $ basenames $ addresses $ demangle $ addr_list)
299+
300+
let () = exit (Cmdliner.Cmd.eval cmd)

test/dune

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@
9292
(= %{system} "macosx")
9393
(= %{architecture} "arm64"))))
9494

95+
(cram
96+
(deps %{bin:addr2line} hello_world hello_world.dSYM)
97+
(enabled_if
98+
(and
99+
(= %{system} "macosx")
100+
(= %{architecture} "arm64"))))
101+
95102
; =============================================================================
96103
; DWARFDUMP COMPARISON TESTS - MACOS
97104
; =============================================================================

test/test_addr2line.t

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Test addr2line basic functionality
2+
3+
Test with function names (-f flag):
4+
$ addr2line -e hello_world.dSYM/Contents/Resources/DWARF/hello_world -f 0x100000478
5+
main
6+
/Users/tsmc/code/ocaml/durin/_build/default/test/hello_world.c:3
7+
8+
Test without function names:
9+
$ addr2line -e hello_world.dSYM/Contents/Resources/DWARF/hello_world 0x100000478
10+
/Users/tsmc/code/ocaml/durin/_build/default/test/hello_world.c:3
11+
12+
Test multiple addresses:
13+
$ addr2line -e hello_world.dSYM/Contents/Resources/DWARF/hello_world -f 0x100000478 0x100000488
14+
main
15+
/Users/tsmc/code/ocaml/durin/_build/default/test/hello_world.c:3
16+
main
17+
/Users/tsmc/code/ocaml/durin/_build/default/test/hello_world.c:4
18+
19+
Test invalid address (should return ??:0):
20+
$ addr2line -e hello_world.dSYM/Contents/Resources/DWARF/hello_world 0xFFFFFFFF
21+
??:0

0 commit comments

Comments
 (0)