|
1 | 1 | -- Scan and dump likely vtable addresses |
2 | | -memscan = require('memscan') |
| 2 | +local memscan = require('memscan') |
3 | 3 |
|
4 | 4 | local osType = dfhack.getOSType() |
5 | 5 | if osType ~= 'linux' then |
6 | 6 | qerror('unsupported OS: ' .. osType) |
7 | 7 | end |
8 | 8 |
|
9 | | -local df_ranges = {} |
10 | | -for _, mem in ipairs(dfhack.internal.getMemRanges()) do |
11 | | - if mem.read and ( |
12 | | - string.match(mem.name,'/dwarfort%.exe$') |
13 | | - or string.match(mem.name,'/dwarfort$') |
14 | | - or string.match(mem.name,'/Dwarf_Fortress$') |
15 | | - or string.match(mem.name,'Dwarf Fortress%.exe') |
16 | | - or string.match(mem.name,'/libg_src_lib.so$') |
17 | | - ) |
18 | | - then |
19 | | - table.insert(df_ranges, mem) |
| 9 | +local function get_ranges() |
| 10 | + local df_ranges, lib_names = {}, {} |
| 11 | + |
| 12 | + local raw_ranges = dfhack.internal.getMemRanges() |
| 13 | + |
| 14 | + -- add main binary mem ranges first |
| 15 | + for _, range in ipairs(raw_ranges) do |
| 16 | + if range.read and ( |
| 17 | + string.match(range.name, '/dwarfort$') or |
| 18 | + string.match(range.name, 'Dwarf Fortress%.exe') |
| 19 | + ) |
| 20 | + then |
| 21 | + table.insert(df_ranges, range) |
| 22 | + end |
| 23 | + end |
| 24 | + |
| 25 | + for _, range in ipairs(raw_ranges) do |
| 26 | + if range.read and string.match(range.name, '/libg_src_lib.so$') then |
| 27 | + table.insert(df_ranges, range) |
| 28 | + lib_names[range.name] = true |
| 29 | + end |
| 30 | + end |
| 31 | + |
| 32 | + return df_ranges, lib_names |
| 33 | +end |
| 34 | + |
| 35 | +local df_ranges, lib_names = get_ranges() |
| 36 | + |
| 37 | +-- vtables that cross a range boundary can appear twice, a truncated version in the |
| 38 | +-- lower memory range and a full version in the higher memory range |
| 39 | +-- therefore, sort the memory ranges by start address, descending |
| 40 | +-- but keep libraries last |
| 41 | +local function sort_ranges(a, b) |
| 42 | + if lib_names[a.name] == lib_names[b.name] then |
| 43 | + return a.start_addr > b.start_addr |
20 | 44 | end |
| 45 | + return lib_names[b.name] |
21 | 46 | end |
22 | 47 |
|
| 48 | +table.sort(df_ranges, sort_ranges) |
| 49 | + |
23 | 50 | function is_df_addr(a) |
24 | | - for _, mem in ipairs(df_ranges) do |
25 | | - if a >= mem.start_addr and a < mem.end_addr then |
| 51 | + for _, range in ipairs(df_ranges) do |
| 52 | + if a >= range.start_addr and a < range.end_addr then |
26 | 53 | return true |
27 | 54 | end |
28 | 55 | end |
29 | 56 | return false |
30 | 57 | end |
31 | 58 |
|
32 | | -local names = {} |
| 59 | +local function is_vtable_range(range) |
| 60 | + return not range.write and not range.execute |
| 61 | +end |
33 | 62 |
|
34 | | -function scan_ranges(g_src) |
| 63 | +function scan_ranges() |
35 | 64 | local vtables = {} |
36 | | - for _, range in ipairs(df_ranges) do |
37 | | - if (not range.read) or range.write or range.execute then |
38 | | - goto next_range |
39 | | - end |
40 | | - if not not range.name:match('g_src') ~= g_src then |
41 | | - goto next_range |
42 | | - end |
| 65 | + local seen = {} -- only record the first encountered vtable for each name |
43 | 66 |
|
| 67 | + for _, range in ipairs(df_ranges) do |
| 68 | + if not is_vtable_range(range) then goto next_range end |
44 | 69 | local base = range.name:match('.*/(.*)$') |
45 | 70 | local area = memscan.MemoryArea.new(range.start_addr, range.end_addr) |
| 71 | + local is_lib = lib_names[range.name] |
46 | 72 | for i = 1, area.uintptr_t.count - 1 do |
47 | 73 | -- take every pointer-aligned value in memory mapped to the DF executable, and see if it is a valid vtable |
48 | 74 | -- start by following the logic in Process::doReadClassName() and ensure it doesn't crash |
@@ -71,27 +97,26 @@ function scan_ranges(g_src) |
71 | 97 | if demangled_name and |
72 | 98 | not demangled_name:match('[<>]') and |
73 | 99 | not demangled_name:match('^std::') and |
74 | | - not names[demangled_name] and |
75 | | - (g_src or demangled_name ~= 'widgets::widget') -- the widget in g_src takes precedence |
| 100 | + not seen[demangled_name] and |
| 101 | + (is_lib or demangled_name ~= 'widgets::widget') -- the widget in g_src takes precedence |
76 | 102 | then |
77 | 103 | local base_str = '' |
78 | | - if g_src then |
| 104 | + if is_lib then |
79 | 105 | vtable = vtable - range.base_addr |
80 | 106 | base_str = (" base='%s'"):format(base) |
81 | 107 | end |
82 | 108 | vtables[demangled_name] = {value=vtable, base_str=base_str} |
| 109 | + seen[demangled_name] = true |
83 | 110 | end |
84 | 111 | ::next_ptr:: |
85 | 112 | end |
86 | 113 | ::next_range:: |
87 | 114 | end |
88 | | - for name, data in pairs(vtables) do |
89 | | - if not names[name] then |
90 | | - print(("<vtable-address name='%s' value='0x%x'%s/>"):format(name, data.value, data.base_str)) |
91 | | - names[name] = true |
92 | | - end |
93 | | - end |
| 115 | + |
| 116 | + return vtables |
94 | 117 | end |
95 | 118 |
|
96 | | -scan_ranges(false) |
97 | | -scan_ranges(true) |
| 119 | +local vtables = scan_ranges() |
| 120 | +for name, data in pairs(vtables) do |
| 121 | + print(("<vtable-address name='%s' value='0x%x'%s/>"):format(name, data.value, data.base_str)) |
| 122 | +end |
0 commit comments