rucc_debug/line.rs
1//! The line table, as the bytes of the sections it goes in.
2//!
3//! Design: `spec/11-asm-objects-debug.md` section 11.4.
4//!
5//! A line table answers one question: given an address in the program, which line of which file was
6//! the compiler writing code for when it produced the instruction there. Everything else DWARF
7//! describes is about what the program means, and this is about where it came from, which is why it
8//! is a table of its own rather than an attribute on something.
9//!
10//! # Why this is the first part written
11//!
12//! Because of what a safety report is. The monitor's descriptor carries a judgement, a class, an
13//! access size and a program counter, and `spec/safe-memory/06-instrumentation.md` section 6.5
14//! deliberately keeps the source location out of it so that a compiler does not ship two line
15//! tables that can come to disagree. That is the right design and it only pays when there is one
16//! line table, and until now there was none, so every report from a corpus run had to be read
17//! backwards out of a disassembly. On a quarter of a million lines of SQLite that is the difference
18//! between a minute and an afternoon per shape.
19//!
20//! # What is here
21//!
22//! The line number program, in `.debug_line`, with the file and directory tables DWARF 5 puts in
23//! its header, and the strings those tables name, in `.debug_line_str`. Beside them the smallest
24//! compilation unit that makes them findable: one `DW_TAG_compile_unit` in `.debug_info` with the
25//! producer, the name of the file, the directory the compiler ran in, a `DW_AT_stmt_list` pointing
26//! at the program and a `DW_AT_ranges` saying which addresses this unit covers, and the abbreviation
27//! it is written against in `.debug_abbrev`. A reader that is handed an address walks the units,
28//! and a unit with no entry in `.debug_info` is a unit nothing walks, so the table alone would have
29//! been a section no tool reads.
30//!
31//! The ranges are a list with one entry per function rather than a low and a high address over the
32//! whole unit. Under `-ffunction-sections` each function is a section of its own and the linker may
33//! place them anywhere and drop the ones nothing reaches, so there is no single span that covers
34//! them, and writing one would be writing down something that is true of the object and false of
35//! the program.
36//!
37//! # What is not here
38//!
39//! Everything about what the program means: no `DW_TAG_subprogram`, no types, no variables and no
40//! location expressions. Those are M8 and tamnd/rucc#9, and what makes them a different piece of
41//! work rather than more of this one is that they are checked differently: a line table is right or
42//! wrong against `addr2line` and a description of a variable is right or wrong against a debugger
43//! that stops in the middle of a function and prints it.
44//!
45//! One sequence per function, each beginning at that function's own symbol. A sequence is the unit
46//! of address ordering in a line program and its rows have to run forwards, so a table with one
47//! sequence over a section would be a table that breaks the moment two functions are laid out in an
48//! order the source did not have. One per function costs a `DW_LNE_set_address` and a relocation
49//! each and is correct under every combination of flags there is.
50
51use rucc_object::{Chunk, Info, Reference, Reloc};
52
53/// One compilation unit's worth of line information.
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct Unit {
56 /// The file being compiled, as the command line spelled it, already prefix mapped.
57 pub name: String,
58 /// The directory the compiler was run in, already prefix mapped.
59 ///
60 /// This is `DW_AT_comp_dir`, and what it is for is that every relative name in the tables below
61 /// is relative to it. A build that cannot say where it ran writes a single dot, which is what
62 /// the tables are already relative to and is therefore the one answer that changes nothing.
63 pub dir: String,
64 /// What produced this, which is this compiler and its version.
65 pub producer: String,
66 /// Every file any row names, in the order the rows refer to them by.
67 pub files: Vec<String>,
68 /// The functions, in the order the text section holds them.
69 pub funcs: Vec<Function>,
70 /// How many bytes an address is on this target.
71 pub pointer: u8,
72}
73
74/// One function, and where each of its instructions came from.
75#[derive(Debug, Clone, Default, PartialEq, Eq)]
76pub struct Function {
77 /// Its name, as the C program spelled it, which is what a relocation here asks the linker for.
78 pub name: String,
79 /// How many bytes of instructions it is.
80 pub len: u64,
81 /// The rows, in increasing order of address.
82 pub rows: Vec<Row>,
83}
84
85/// One row of the table: an address, and where the code at it came from.
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87pub struct Row {
88 /// How far into its function the instruction is.
89 pub at: u64,
90 /// Which of [`Unit::files`] it is in.
91 pub file: usize,
92 /// Which line of that file, counting from one, or zero for code no line of any file asked for.
93 ///
94 /// Zero is DWARF's own spelling of that and is worth more than a guess: a debugger stepping
95 /// over a row with no line knows not to stop, where one handed the nearest line it could find
96 /// would stop somewhere the program never was.
97 pub line: u32,
98 /// Which column of that line, counting from one, or zero for the left edge.
99 pub column: u32,
100}
101
102/// What went wrong while the sections were being built.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum Error {
105 /// The DWARF writer refused something, which is a bug here rather than in a program.
106 Refused {
107 /// What it said, already formatted.
108 why: String,
109 },
110}
111
112impl std::fmt::Display for Error {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 Error::Refused { why } => {
116 write!(f, "the debug writer refused what it was given: {why}")
117 }
118 }
119 }
120}
121
122impl std::error::Error for Error {}
123
124/// One section of DWARF being written, and the relocations found while writing it.
125///
126/// The writer underneath hands a relocation over the moment it writes the hole rather than at the
127/// end, because the hole's offset is the length of what it has written so far, so this has to be
128/// one type rather than bytes now and relocations later.
129#[derive(Debug, Clone)]
130struct Section {
131 bytes: gimli::write::EndianVec<gimli::LittleEndian>,
132 relocs: Vec<gimli::write::Relocation>,
133}
134
135impl Default for Section {
136 fn default() -> Self {
137 Self { bytes: gimli::write::EndianVec::new(gimli::LittleEndian), relocs: Vec::new() }
138 }
139}
140
141impl gimli::write::RelocateWriter for Section {
142 type Writer = gimli::write::EndianVec<gimli::LittleEndian>;
143
144 fn writer(&self) -> &Self::Writer {
145 &self.bytes
146 }
147
148 fn writer_mut(&mut self) -> &mut Self::Writer {
149 &mut self.bytes
150 }
151
152 fn relocate(&mut self, relocation: gimli::write::Relocation) {
153 self.relocs.push(relocation);
154 }
155}
156
157/// The sections a unit's line information goes in.
158///
159/// The result is empty when nothing in the unit has a row, which is a file of declarations and a
160/// file whose every function was dropped. An empty `.debug_line` is worse than no section at all,
161/// since a reader would find a unit covering no addresses and have to decide what that meant.
162///
163/// # Errors
164///
165/// [`Error::Refused`] for anything the DWARF writer objected to. Every value it is handed here came
166/// out of this compiler, so that is a bug here rather than a program's mistake.
167pub fn write(unit: &Unit) -> Result<Info, Error> {
168 if unit.funcs.iter().all(|func| func.rows.is_empty()) {
169 return Ok(Info::default());
170 }
171 let encoding =
172 gimli::Encoding { format: gimli::Format::Dwarf32, version: 5, address_size: unit.pointer };
173 let mut dwarf = gimli::write::DwarfUnit::new(encoding);
174 let dir = text(&unit.dir, encoding, &mut dwarf.line_strings);
175 let name = text(&unit.name, encoding, &mut dwarf.line_strings);
176 let mut program =
177 gimli::write::LineProgram::new(encoding, gimli::LineEncoding::default(), dir, name, None);
178 // Every file the rows name, under the directory above. The name is written whole rather than
179 // split into a directory and a base, which is legal and is not what gcc does: a reader joins
180 // the two only when the file is relative, so a whole name is one the reader takes as it stands.
181 // Splitting would buy a shorter table on a project whose files share directories and would put
182 // a second place in here where a path is taken apart.
183 let under = program.default_directory();
184 let files: Vec<gimli::write::FileId> = unit
185 .files
186 .iter()
187 .map(|file| {
188 let file = text(file, encoding, &mut dwarf.line_strings);
189 program.add_file(file, under, None)
190 })
191 .collect();
192 for (index, func) in unit.funcs.iter().enumerate() {
193 if func.rows.is_empty() {
194 continue;
195 }
196 program.begin_sequence(Some(gimli::write::Address::Symbol { symbol: index, addend: 0 }));
197 // A row that says what the row before it said is a row a reader would read and discard, so
198 // it is left out. That is most of them: a line of C is several instructions and every one
199 // of them carries the same span.
200 let mut said: Option<(usize, u32, u32)> = None;
201 for row in &func.rows {
202 let now = (row.file, row.line, row.column);
203 if said == Some(now) {
204 continue;
205 }
206 said = Some(now);
207 let Some(&file) = files.get(row.file) else {
208 let why = format!("row at {} names file {}, which is not one", row.at, row.file);
209 return Err(Error::Refused { why });
210 };
211 let state = program.row();
212 state.address_offset = row.at;
213 state.file = file;
214 state.line = u64::from(row.line);
215 state.column = u64::from(row.column);
216 // Every row is somewhere a breakpoint may attach, because at this optimization level
217 // every row is the start of a statement or is code with no statement to be the start
218 // of, and the second kind carries line zero and is not a place a debugger stops.
219 state.is_statement = true;
220 program.generate_row();
221 }
222 program.end_sequence(func.len);
223 }
224 let ranges = unit
225 .funcs
226 .iter()
227 .enumerate()
228 .filter(|(_, func)| !func.rows.is_empty())
229 .map(|(index, func)| gimli::write::Range::StartLength {
230 begin: gimli::write::Address::Symbol { symbol: index, addend: 0 },
231 length: func.len,
232 })
233 .collect();
234 dwarf.unit.line_program = program;
235 let covers = dwarf.unit.ranges.add(gimli::write::RangeList(ranges));
236 let root = dwarf.unit.root();
237 let producer = text(&unit.producer, encoding, &mut dwarf.line_strings);
238 let name = text(&unit.name, encoding, &mut dwarf.line_strings);
239 let dir = text(&unit.dir, encoding, &mut dwarf.line_strings);
240 let root = dwarf.unit.get_mut(root);
241 root.set(gimli::DW_AT_producer, gimli::write::AttributeValue::LineStringRef(held(producer)?));
242 root.set(gimli::DW_AT_language, gimli::write::AttributeValue::Language(gimli::DW_LANG_C11));
243 root.set(gimli::DW_AT_name, gimli::write::AttributeValue::LineStringRef(held(name)?));
244 root.set(gimli::DW_AT_comp_dir, gimli::write::AttributeValue::LineStringRef(held(dir)?));
245 root.set(gimli::DW_AT_stmt_list, gimli::write::AttributeValue::LineProgramRef);
246 root.set(gimli::DW_AT_ranges, gimli::write::AttributeValue::RangeListRef(covers));
247 let mut sections = gimli::write::Sections::new(Section::default());
248 dwarf.write(&mut sections).map_err(refused)?;
249 let mut info = Info::default();
250 let named = |target: gimli::write::RelocationTarget| match target {
251 gimli::write::RelocationTarget::Symbol(index) => unit.funcs[index].name.clone(),
252 gimli::write::RelocationTarget::Section(id) => id.name().to_owned(),
253 };
254 sections.for_each(|id, section| {
255 if section.bytes.slice().is_empty() {
256 return Ok(());
257 }
258 let relocs = section
259 .relocs
260 .iter()
261 .map(|reloc| Reloc {
262 at: reloc.offset,
263 symbol: named(reloc.target),
264 kind: Reference::Address { bytes: reloc.size },
265 addend: reloc.addend,
266 after: 0,
267 })
268 .collect();
269 info.chunks.push(Chunk {
270 name: id.name().to_owned(),
271 bytes: section.bytes.slice().to_vec(),
272 relocs,
273 });
274 Ok::<(), Error>(())
275 })?;
276 Ok(info)
277}
278
279/// A string as the line program writes one, which is a reference into `.debug_line_str`.
280///
281/// Every string here goes in that section rather than in `.debug_str` or inline, because the file
282/// and directory tables of a DWARF 5 line program can reach it and the unit's own attributes can
283/// too, so one section holds all of them and a name that appears in both is written once.
284fn text(
285 val: &str,
286 encoding: gimli::Encoding,
287 strings: &mut gimli::write::LineStringTable,
288) -> gimli::write::LineString {
289 // A null byte in a path is not something a file system hands back and is something the writer
290 // underneath panics on, so it is taken out rather than passed through.
291 let val: Vec<u8> = val.bytes().filter(|&byte| byte != 0).collect();
292 gimli::write::LineString::new(val, encoding, strings)
293}
294
295/// The identifier behind a string that went into `.debug_line_str`.
296///
297/// [`text`] answers with whichever form of string the encoding wanted, and for DWARF 5 that is
298/// always a reference into that section. An attribute has to name the reference rather than repeat
299/// the bytes, so this is where the one shape the encoding can produce is taken apart, and anything
300/// else is a disagreement between this function and that one rather than anything a caller did.
301fn held(string: gimli::write::LineString) -> Result<gimli::write::LineStringId, Error> {
302 match string {
303 gimli::write::LineString::LineStringRef(id) => Ok(id),
304 _ => Err(Error::Refused {
305 why: "a string meant for the line string section was written another way".to_owned(),
306 }),
307 }
308}
309
310/// What the DWARF writer said, as the one kind of news it can be here.
311fn refused(why: gimli::write::Error) -> Error {
312 Error::Refused { why: why.to_string() }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 /// A unit with one function and two lines in it.
320 fn one() -> Unit {
321 Unit {
322 name: "a.c".to_owned(),
323 dir: "/tmp".to_owned(),
324 producer: "rucc".to_owned(),
325 files: vec!["a.c".to_owned()],
326 funcs: vec![Function {
327 name: "f".to_owned(),
328 len: 16,
329 rows: vec![
330 Row { at: 0, file: 0, line: 3, column: 1 },
331 Row { at: 8, file: 0, line: 4, column: 5 },
332 ],
333 }],
334 pointer: 8,
335 }
336 }
337
338 /// The sections that come out, and that each of them has something in it.
339 ///
340 /// Four rather than two, because a line table nothing can find is a section no reader opens.
341 /// The unit in `.debug_info` is what a reader walks to reach the program, the abbreviation in
342 /// `.debug_abbrev` is what that unit is written against, and the strings are in
343 /// `.debug_line_str` because both the unit and the program's own tables name them.
344 #[test]
345 fn a_unit_with_rows_writes_the_four_sections_a_reader_needs() {
346 let info = write(&one()).expect("sections");
347 let names: Vec<&str> = info.chunks.iter().map(|chunk| chunk.name.as_str()).collect();
348 assert_eq!(
349 names,
350 [".debug_abbrev", ".debug_line_str", ".debug_line", ".debug_rnglists", ".debug_info"]
351 );
352 assert!(info.chunks.iter().all(|chunk| !chunk.bytes.is_empty()));
353 }
354
355 /// Where a function is is the one number no compilation knows, so every sequence asks for it.
356 ///
357 /// The relocation names the function rather than the section it is in, because under
358 /// `-ffunction-sections` the section is the function's own and under anything else the object
359 /// writer is the one that knows where in the text it landed. The others in the same section are
360 /// the header naming its own strings, which is the other thing only a linker can resolve.
361 #[test]
362 fn a_sequence_asks_the_linker_where_its_function_went() {
363 let info = write(&one()).expect("sections");
364 let line = info.chunks.iter().find(|chunk| chunk.name == ".debug_line").expect("a table");
365 let address = line.relocs.iter().find(|reloc| reloc.symbol == "f").expect("an address");
366 assert_eq!(address.kind, Reference::Address { bytes: 8 });
367 assert_eq!(address.addend, 0);
368 // The rest are the header's own, and they are section offsets rather than addresses: a
369 // directory and a file name in DWARF 5 are written as a place in `.debug_line_str`.
370 let rest = line.relocs.iter().filter(|reloc| reloc.symbol != "f");
371 assert!(rest.clone().count() > 0);
372 assert!(rest.clone().all(|reloc| reloc.symbol == ".debug_line_str"));
373 assert!(rest.clone().all(|reloc| reloc.kind == Reference::Address { bytes: 4 }));
374 }
375
376 /// A file with nothing to say writes no sections rather than empty ones.
377 #[test]
378 fn a_unit_with_no_rows_writes_nothing() {
379 let mut unit = one();
380 unit.funcs[0].rows.clear();
381 assert_eq!(write(&unit).expect("sections"), Info::default());
382 }
383
384 /// A row naming a file the unit does not have is refused rather than written as something else.
385 #[test]
386 fn a_row_naming_a_file_that_is_not_there_is_refused() {
387 let mut unit = one();
388 unit.funcs[0].rows[1].file = 7;
389 assert!(write(&unit).is_err());
390 }
391}