aboutsummaryrefslogtreecommitdiff
path: root/lib/xixanta/src/assembler.rs
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-04-08 16:04:12 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-04-08 16:04:12 +0200
commita2cb050ca18fd7c5533d69e9aa093e4f0a40d0fa (patch)
tree9ee9c3c72804c7e78c4dc3e7382f29c669f6da35 /lib/xixanta/src/assembler.rs
parentabaffa61df7db8522d29869f7c455c59b8e96dbc (diff)
downloadtools.nes-a2cb050ca18fd7c5533d69e9aa093e4f0a40d0fa.tar.gz
tools.nes-a2cb050ca18fd7c5533d69e9aa093e4f0a40d0fa.zip
Implement the asan:stack statement
This statement allows programmers to reserve a memory range for the stack. This is useful to reserve a memory region which is not attached to any variable, and: 1. It is a way to safely reserve space on the $0100 page. 2. The --stats/--write-info flags will be able to add up the memory space reserved for the stack. 3. Future tooling might be able to use this value as a way to detect stack overflows. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
Diffstat (limited to 'lib/xixanta/src/assembler.rs')
-rw-r--r--lib/xixanta/src/assembler.rs116
1 files changed, 116 insertions, 0 deletions
diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs
index 1b963f2..2f0292e 100644
--- a/lib/xixanta/src/assembler.rs
+++ b/lib/xixanta/src/assembler.rs
@@ -146,6 +146,9 @@ struct Assembler<'a> {
// The amount of bytes to reserve for the next variable assignment.
asan_next_reserve: usize,
+ // The memory range as defined by an 'asan:stack' statement.
+ asan_stack_definition: Option<MemoryRange>,
+
// Stack of macro expansions. That is, every time a macro is expanded
// (i.e. via 'bundle_call'), the node that performed the expansion is
// stacked here. This is then picked up via structs like 'PendingNode' and
@@ -305,6 +308,27 @@ pub fn assemble_with_mapping(
};
}
+ // We have enough information for asan:stack after evaluating the
+ // context. So, if asan is enabled, account for it now, otherwise push a
+ // warning.
+ if asm.asan_enabled {
+ match asm.asan_stack_definition {
+ Some(ref mr) => {
+ memory.memory_ranges.push(mr.clone());
+ memory.total_internal_ram += mr.range.len();
+ }
+ None => {
+ asm.warnings.push(Error {
+ line: 0,
+ message: "you have not defined a range for 'asan:stack'".to_string(),
+ source: source.clone(),
+ global: true,
+ expanded_from: vec![],
+ });
+ }
+ }
+ }
+
// Convert the relevant nodes into binary bundles which can be used by
// the caller. This is done for most nodes, even if some of them will
// have to be marked as pending, since they depend on knowing the exact
@@ -394,6 +418,7 @@ impl<'a> Assembler<'a> {
asan_enabled: false,
asan_next_ignore: false,
asan_next_reserve: 1,
+ asan_stack_definition: None,
macro_context: vec![],
pending_defines: vec![],
}
@@ -525,6 +550,28 @@ impl<'a> Assembler<'a> {
}
}
}
+ NodeType::Comment(CommentType::AsanStack(range)) => {
+ match self.asan_stack_definition {
+ Some(_) => {
+ if self.asan_stack_definition.is_some() {
+ errors.push(Error {
+ message: "you have already defined a value for 'asan:stack'"
+ .to_string(),
+ line: node.value.line,
+ source: self.source_for(node),
+ expanded_from: self.macro_context.clone(),
+ global: false,
+ });
+ }
+ }
+ None => {
+ self.asan_stack_definition = Some(MemoryRange {
+ range: range.clone(),
+ name: "<stack>".to_string(),
+ });
+ }
+ }
+ }
NodeType::Comment(CommentType::AsanIgnore) => {
self.asan_next_ignore = true;
}
@@ -3618,6 +3665,75 @@ mod tests {
}
#[test]
+ fn asan_no_stack() {
+ let line = "nop";
+ let real_line = minimal_header().to_string() + line;
+
+ let res = assemble_with_mapping(
+ real_line.as_bytes(),
+ empty(),
+ &[],
+ &SourceInfo::default(),
+ true,
+ );
+
+ assert_eq!(res.warnings.len(), 1);
+ assert_eq!(
+ res.warnings[0].to_string(),
+ "you have not defined a range for 'asan:stack'"
+ );
+ }
+
+ #[test]
+ fn asan_dup_stack() {
+ let line = r#";; asan:stack $00-$FF
+;; asan:stack $00-$FF
+"#;
+
+ let real_line = minimal_header().to_string() + line;
+
+ let res = assemble_with_mapping(
+ real_line.as_bytes(),
+ empty(),
+ &[],
+ &SourceInfo::default(),
+ true,
+ );
+
+ assert_eq!(res.errors.len(), 1);
+
+ // NOTE: 3 lines for the "minimal header" being prepended.
+ assert_eq!(
+ res.errors[0].to_string(),
+ "you have already defined a value for 'asan:stack' (line 5)"
+ );
+ }
+
+ #[test]
+ fn asan_stack() {
+ let line = "nop ;; asan:stack $00-$FF";
+ let real_line = minimal_header().to_string() + line;
+
+ let res = assemble_with_mapping(
+ real_line.as_bytes(),
+ empty(),
+ &[],
+ &SourceInfo::default(),
+ true,
+ );
+
+ assert_eq!(res.errors.len(), 0);
+ assert_eq!(res.warnings.len(), 0);
+ assert_eq!(res.memory.memory_ranges.len(), 1);
+
+ let mr = res.memory.memory_ranges.first().unwrap();
+ assert_eq!(mr.range, (0x100..0x200));
+ assert_eq!(mr.name, "<stack>".to_string());
+
+ assert_eq!(res.memory.total_internal_ram, 0x100);
+ }
+
+ #[test]
fn asan_invalid_negative_variable() {
let line = "m_var = -1";
let real_line = minimal_header().to_string() + line;