Skip to content

Commit ddc420c

Browse files
vaivaswathatritaosdankel
authored
Initial DWARF debug symbols output code. (#5521)
This PR adds support to express line number mapping from assembly to source using DWARF object files. Since there are offset details that are specific to the Fuel ISA, I'm using the existing `SourceMap` that's already being built, rather than generating directly from the instructions. An important point to note is that DWARF uses line-col to express positions rather than an absolute offset-from-start format. To avoid computing line-col from absolute positions (which requires re-reading the source files), I'm changing the source position format everywhere to line-col. This also simplifies our code at many places (for example, in `forc-debug`). The only exception is the `addr2line` tool, where the use of absolute positions is in a function that seemed quite complex to me to attempt changes. So I have, temporarily, added code to compute absolute positions from line-col information and then reuse the existing functions. This is inefficient, but I think that's _okay_ since it's a standalone tool and the whole thing runs just once for every command invocation. The DWARF information is written to a file specified by the `-g` flag and can be verified using `llvm-dwarfdump --debug-line ./test.obj`. If the argument to the `-g` flag is a filename that ends with `.json`, then the existing JSON format is written out, otherwise it's a DWARF object in an ELF container. --------- Co-authored-by: Joao Matos <joao@tritao.eu> Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com>
1 parent a81e481 commit ddc420c

File tree

19 files changed

+489
-285
lines changed

19 files changed

+489
-285
lines changed

Cargo.lock

+218-172
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

forc-pkg/src/pkg.rs

+15-7
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use sway_core::{
4747
semantic_analysis::namespace,
4848
source_map::SourceMap,
4949
transform::AttributeKind,
50-
BuildTarget, Engines, FinalizedEntry, LspConfig,
50+
write_dwarf, BuildTarget, Engines, FinalizedEntry, LspConfig,
5151
};
5252
use sway_error::{error::CompileError, handler::Handler, warning::CompileWarning};
5353
use sway_types::constants::{CORE, PRELUDE, STD};
@@ -293,7 +293,9 @@ pub struct BuildOpts {
293293
pub minify: MinifyOpts,
294294
/// If set, outputs a binary file representing the script bytes.
295295
pub binary_outfile: Option<String>,
296-
/// If set, outputs source file mapping in JSON format
296+
/// If set, outputs debug info to the provided file.
297+
/// If the argument provided ends with .json, a JSON is emitted,
298+
/// otherwise, an ELF file containing DWARF is emitted.
297299
pub debug_outfile: Option<String>,
298300
/// Build target to use.
299301
pub build_target: BuildTarget,
@@ -426,11 +428,17 @@ impl BuiltPackage {
426428
Ok(())
427429
}
428430

429-
/// Writes debug_info (source_map) of the BuiltPackage to the given `path`.
430-
pub fn write_debug_info(&self, path: &Path) -> Result<()> {
431-
let source_map_json =
432-
serde_json::to_vec(&self.source_map).expect("JSON serialization failed");
433-
fs::write(path, source_map_json)?;
431+
/// Writes debug_info (source_map) of the BuiltPackage to the given `out_file`.
432+
pub fn write_debug_info(&self, out_file: &Path) -> Result<()> {
433+
if matches!(out_file.extension(), Some(ext) if ext == "json") {
434+
let source_map_json =
435+
serde_json::to_vec(&self.source_map).expect("JSON serialization failed");
436+
fs::write(out_file, source_map_json)?;
437+
} else {
438+
let primary_dir = self.descriptor.manifest_file.dir();
439+
let primary_src = self.descriptor.manifest_file.entry_path();
440+
write_dwarf(&self.source_map, primary_dir, &primary_src, out_file)?;
441+
}
434442
Ok(())
435443
}
436444

forc-plugins/forc-debug/src/server/handlers/handle_launch.rs

+20-43
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use forc_pkg::{self, BuildProfile, Built, BuiltPackage, PackageManifestFile};
55
use forc_test::execute::TestExecutor;
66
use forc_test::setup::TestSetup;
77
use forc_test::BuiltTests;
8-
use std::{collections::HashMap, fs, sync::Arc};
9-
use sway_types::span::Position;
8+
use std::{collections::HashMap, sync::Arc};
9+
use sway_types::LineCol;
1010

1111
impl DapServer {
1212
/// Handles a `launch` request. Returns true if the server should continue running.
@@ -124,50 +124,27 @@ impl DapServer {
124124
let source_map = &built_pkg.source_map;
125125

126126
let paths = &source_map.paths;
127-
// Cache the source code for every path in the map, since we'll need it later.
128-
let source_code = paths
129-
.iter()
130-
.filter_map(|path_buf| {
131-
if let Ok(source) = fs::read_to_string(path_buf) {
132-
Some((path_buf, source))
133-
} else {
134-
None
135-
}
136-
})
137-
.collect::<HashMap<_, _>>();
138-
139127
source_map.map.iter().for_each(|(instruction, sm_span)| {
140128
if let Some(path_buf) = paths.get(sm_span.path.0) {
141-
if let Some(source_code) = source_code.get(path_buf) {
142-
if let Some(start_pos) = Position::new(source_code, sm_span.range.start) {
143-
let (line, _) = start_pos.line_col();
144-
let (line, instruction) = (line as i64, *instruction as Instruction);
145-
146-
self.state
147-
.source_map
148-
.entry(path_buf.clone())
149-
.and_modify(|new_map| {
150-
new_map
151-
.entry(line)
152-
.and_modify(|val| {
153-
// Store the instructions in ascending order
154-
match val.binary_search(&instruction) {
155-
Ok(_) => {} // Ignore duplicates
156-
Err(pos) => val.insert(pos, instruction),
157-
}
158-
})
159-
.or_insert(vec![instruction]);
129+
let LineCol { line, .. } = sm_span.range.start;
130+
let (line, instruction) = (line as i64, *instruction as Instruction);
131+
132+
self.state
133+
.source_map
134+
.entry(path_buf.clone())
135+
.and_modify(|new_map| {
136+
new_map
137+
.entry(line)
138+
.and_modify(|val| {
139+
// Store the instructions in ascending order
140+
match val.binary_search(&instruction) {
141+
Ok(_) => {} // Ignore duplicates
142+
Err(pos) => val.insert(pos, instruction),
143+
}
160144
})
161-
.or_insert(HashMap::from([(line, vec![instruction])]));
162-
} else {
163-
self.error(format!(
164-
"Couldn't get position: {:?} in file: {:?}",
165-
sm_span.range.start, path_buf
166-
));
167-
}
168-
} else {
169-
self.error(format!("Couldn't read file: {:?}", path_buf));
170-
}
145+
.or_insert(vec![instruction]);
146+
})
147+
.or_insert(HashMap::from([(line, vec![instruction])]));
171148
} else {
172149
self.error(format!(
173150
"Path missing from source map: {:?}",

forc-test/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ pub struct TestOpts {
131131
pub minify: pkg::MinifyOpts,
132132
/// If set, outputs a binary file representing the script bytes.
133133
pub binary_outfile: Option<String>,
134-
/// If set, outputs source file mapping in JSON format
134+
/// If set, outputs debug info to the provided file.
135+
/// If the argument provided ends with .json, a JSON is emitted,
136+
/// otherwise, an ELF file containing DWARF is emitted.
135137
pub debug_outfile: Option<String>,
136138
/// Build target to use.
137139
pub build_target: BuildTarget,

forc/src/cli/commands/addr2line.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::VecDeque;
55
use std::fs::{self, File};
66
use std::io::{self, prelude::*, BufReader};
77
use std::path::{Path, PathBuf};
8+
use sway_types::LineCol;
89
use tracing::info;
910

1011
use annotate_snippets::{AnnotationType, Slice, Snippet, SourceAnnotation};
@@ -82,11 +83,29 @@ struct ReadRange {
8283
length: usize,
8384
}
8485

86+
fn line_col_to_pos<P: AsRef<Path>>(&LineCol { line, col }: &LineCol, path: P) -> io::Result<usize> {
87+
let file = File::open(&path)?;
88+
let mut reader = BufReader::new(file);
89+
90+
let mut pos = 0usize;
91+
let mut buffer = String::new();
92+
for _line_count in 1..line {
93+
buffer.clear();
94+
pos += reader.read_line(&mut buffer)?;
95+
}
96+
Ok(pos + col)
97+
}
98+
8599
fn read_range<P: AsRef<Path>>(
86100
path: P,
87101
range: LocationRange,
88102
context_lines: usize,
89103
) -> io::Result<ReadRange> {
104+
// Converting LineCol to Pos, twice, is inefficient.
105+
// TODO: Rewrite the algorithm in terms of LineCol.
106+
let range_start = line_col_to_pos(&range.start, &path)?;
107+
let range_end = line_col_to_pos(&range.end, &path)?;
108+
90109
let file = File::open(&path)?;
91110
let mut reader = BufReader::new(file);
92111
let mut context_buffer = VecDeque::new();
@@ -98,9 +117,9 @@ fn read_range<P: AsRef<Path>>(
98117
let n = reader.read_line(&mut buffer)?;
99118
context_buffer.push_back(buffer);
100119
if start_pos.is_none() {
101-
if position + n > range.start {
120+
if position + n > range_start {
102121
let cbl: usize = context_buffer.iter().map(|c| c.len()).sum();
103-
start_pos = Some((line_num, position, range.start - (position + n - cbl)));
122+
start_pos = Some((line_num, position, range_start - (position + n - cbl)));
104123
} else if context_buffer.len() > context_lines {
105124
let _ = context_buffer.pop_front();
106125
}
@@ -112,7 +131,7 @@ fn read_range<P: AsRef<Path>>(
112131
}
113132

114133
let source = context_buffer.make_contiguous().join("");
115-
let length = range.end - range.start;
134+
let length = range_end - range_start;
116135

117136
let (source_start_line, _source_start_byte, offset) = start_pos.ok_or_else(|| {
118137
io::Error::new(

forc/src/cli/shared.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ pub struct Build {
2626
/// Build output file options.
2727
#[derive(Args, Debug, Default)]
2828
pub struct BuildOutput {
29-
/// If set, outputs a binary file representing the script bytes.
29+
/// Create a binary file representing the script bytecode at the provided path.
3030
#[clap(long = "output-bin", short = 'o')]
3131
pub bin_file: Option<String>,
32-
/// If set, outputs source file mapping in JSON format
32+
/// Create a file containing debug information at the provided path.
33+
/// If the file extension is .json, JSON format is used. Otherwise, an ELF file containing DWARF is emitted.
3334
#[clap(long = "output-debug", short = 'g')]
3435
pub debug_file: Option<String>,
3536
}

sway-core/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ etk-dasm = { package = "fuel-etk-dasm", version = "0.3.1-dev" }
2121
etk-ops = { package = "fuel-etk-ops", version = "0.3.1-dev" }
2222
fuel-abi-types = { workspace = true }
2323
fuel-vm = { workspace = true, features = ["serde"] }
24+
gimli = "0.28.1"
2425
graph-cycles = "0.1.0"
2526
hashbrown = "0.13.1"
2627
hex = { version = "0.4", optional = true }
@@ -29,6 +30,7 @@ indexmap = "2.0.0"
2930
itertools = "0.10"
3031
lazy_static = "1.4"
3132
miden-core = "0.3.0"
33+
object = { version = "0.32.2", features = ["write"] }
3234
pest = "2.1.3"
3335
pest_derive = "2.1"
3436
petgraph = "0.6"

sway-core/src/asm_generation/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ mod miden_vm;
77
pub use miden_vm::*;
88
pub mod from_ir;
99
pub mod fuel;
10-
mod instruction_set;
10+
pub mod instruction_set;
1111
mod programs;
1212

1313
mod finalized_asm;

sway-core/src/control_flow_analysis/flow_graph/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
transform, Engines, Ident,
1111
};
1212

13-
use sway_types::{span::Span, BaseIdent, IdentUnique, Spanned};
13+
use sway_types::{span::Span, BaseIdent, IdentUnique, LineCol, Spanned};
1414

1515
use petgraph::{graph::EdgeIndex, prelude::NodeIndex};
1616

@@ -239,7 +239,7 @@ impl<'cfg> ControlFlowGraph<'cfg> {
239239
if let Some(source_id) = span.source_id() {
240240
let path = engines.se().get_path(source_id);
241241
let path = path.to_string_lossy();
242-
let (line, col) = span.start_pos().line_col();
242+
let LineCol { line, col } = span.start_pos().line_col();
243243
let url_format = url_format
244244
.replace("{path}", path.to_string().as_str())
245245
.replace("{line}", line.to_string().as_str())
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use std::fs::File;
2+
use std::os::unix::ffi::OsStringExt;
3+
use std::path::Path;
4+
5+
use gimli::write::{
6+
self, DebugLine, DebugLineStrOffsets, DebugStrOffsets, DwarfUnit, EndianVec, LineProgram,
7+
LineString,
8+
};
9+
use gimli::{BigEndian, Encoding, LineEncoding};
10+
use sway_error::error::CompileError;
11+
use sway_types::Span;
12+
13+
use crate::source_map::SourceMap;
14+
15+
use object::write::Object;
16+
17+
pub fn write_dwarf(
18+
source_map: &SourceMap,
19+
primary_dir: &Path,
20+
primary_src: &Path,
21+
out_file: &Path,
22+
) -> Result<(), CompileError> {
23+
let encoding = gimli::Encoding {
24+
format: gimli::Format::Dwarf64,
25+
version: 5,
26+
address_size: 8,
27+
};
28+
29+
let program = build_line_number_program(encoding, primary_dir, primary_src, source_map)?;
30+
31+
program
32+
.write(
33+
&mut DebugLine::from(EndianVec::new(BigEndian)),
34+
encoding,
35+
&DebugLineStrOffsets::none(),
36+
&DebugStrOffsets::none(),
37+
)
38+
.map_err(|err| {
39+
sway_error::error::CompileError::InternalOwned(err.to_string(), Span::dummy())
40+
})?;
41+
42+
let mut dwarf = DwarfUnit::new(encoding);
43+
dwarf.unit.line_program = program;
44+
// Write to new sections
45+
let mut debug_sections = write::Sections::new(EndianVec::new(BigEndian));
46+
dwarf.write(&mut debug_sections).map_err(|err| {
47+
sway_error::error::CompileError::InternalOwned(err.to_string(), Span::dummy())
48+
})?;
49+
50+
let file = File::create(out_file).unwrap();
51+
let mut obj = Object::new(
52+
object::BinaryFormat::Elf,
53+
object::Architecture::X86_64,
54+
object::Endianness::Big,
55+
);
56+
debug_sections
57+
.for_each(|section_id, data| {
58+
let sec = obj.add_section(
59+
[].into(),
60+
section_id.name().into(),
61+
object::SectionKind::Other,
62+
);
63+
obj.set_section_data(sec, data.clone().into_vec(), 8);
64+
Ok::<(), ()>(())
65+
})
66+
.unwrap();
67+
68+
obj.write_stream(file).map_err(|err| {
69+
sway_error::error::CompileError::InternalOwned(err.to_string(), Span::dummy())
70+
})?;
71+
72+
Ok(())
73+
}
74+
75+
fn build_line_number_program(
76+
encoding: Encoding,
77+
primary_dir: &Path,
78+
primary_src: &Path,
79+
source_map: &SourceMap,
80+
) -> Result<LineProgram, CompileError> {
81+
let primary_src = primary_src.strip_prefix(primary_dir).map_err(|err| {
82+
sway_error::error::CompileError::InternalOwned(err.to_string(), Span::dummy())
83+
})?;
84+
let mut program = LineProgram::new(
85+
encoding,
86+
LineEncoding::default(),
87+
LineString::String(primary_dir.to_path_buf().into_os_string().into_vec()),
88+
LineString::String(primary_src.to_path_buf().into_os_string().into_vec()),
89+
None,
90+
);
91+
92+
program.begin_sequence(Some(write::Address::Constant(0)));
93+
94+
for (ix, span) in &source_map.map {
95+
let (path, span) = span.to_span(&source_map.paths, &source_map.dependency_paths);
96+
97+
let dir = path
98+
.parent()
99+
.ok_or(sway_error::error::CompileError::InternalOwned(
100+
"Path doesn't have a proper prefix".to_string(),
101+
Span::dummy(),
102+
))?;
103+
let file = path
104+
.file_name()
105+
.ok_or(sway_error::error::CompileError::InternalOwned(
106+
"Path doesn't have proper filename".to_string(),
107+
Span::dummy(),
108+
))?;
109+
110+
let dir_id = program.add_directory(LineString::String(
111+
dir.as_os_str().as_encoded_bytes().into(),
112+
));
113+
let file_id = program.add_file(
114+
LineString::String(file.as_encoded_bytes().into()),
115+
dir_id,
116+
None,
117+
);
118+
119+
program.generate_row();
120+
121+
let current_row = program.row();
122+
current_row.line = span.start.line as u64;
123+
current_row.column = span.start.col as u64;
124+
current_row.address_offset = *ix as u64;
125+
current_row.file = file_id;
126+
}
127+
128+
program.end_sequence(
129+
source_map
130+
.map
131+
.last_key_value()
132+
.map(|(key, _)| *key)
133+
.unwrap_or_default() as u64
134+
+ 1,
135+
);
136+
137+
Ok(program)
138+
}

sway-core/src/debug_generation/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod dwarf;
2+
pub use dwarf::*;

0 commit comments

Comments
 (0)