nasm is an assembler specifically tailored for the Famicom/NES. Because of
this, it does not try to replace other assemblers available out there which
cover all 6502 platforms and derivatives. The only goal is to have an assembler
that is suitable for NES/Famicom development.
As such, only features available for this platform have been implemented. All in
all, nasm is an assembler that makes a lot of assumptions on the target
platform, its memory layout, and how developers have to write their code. You
can read more about this down below.
Last but not least, nasm makes an effort at being flexible and as informative
to programmers as possible. Hence, error messages try to be concise, clear, and
pointing exactly where each error happened.
Usage
The most basic way to use this assembler is by running:
$ nasm awesome.s
This will produce an out.nes file placed under the same working directory. You
can change the name of the file with the -o/--output flag. Hence, you can call
it like so:
$ nasm -o awesome.nes awesome.s
Moreover, you can actually tell nasm to redirect the output to stdout instead
with the --stdout flag. This is useful when debugging the binary format with
another CLI tool. For example:
$ nasm --stdout awesome.s | hexdump -C
The syntax for this assembler is virtually the same as the one for
ca65, even if some functions might be
missing. One difference that you will find in contrast with ca65 is that
nasm is a bit more pedantic. Let's consider the following example:
.scope Scope
.macro MACRO
lda #0
.endmacro
.endscope
Here ca65 will place MACRO on the global scope. nasm will do the same but
it will also print a warning telling the programmer about this, as this might be
unexpected at first. Hence, as a general rule nasm will be more noisy than
ca65, in the hope that the programmer is more aware about the end result.
That being said, nasm is at the same time more flexible than ca65. For
exemple, the order of declaration for variables, proc's, etc. is not
important. Hence, the following code is valid in nasm, but not in ca65:
.proc foo
;; 'ca65' will complain that 'Variable' is not known.
lda #Variable
rts
.endproc
Variable = $00
Exit code
The exit status code from nasm is the amount of errors that were
detected. Hence, if the exit status code is 0, then everything is fine,
otherwise you will get a count of errors.
Note that warnings that were turned into errors via the -Werror flag will also
be accounted.
Defining global values from the command line
You can define global values with the -D flag which follows a NAME=VALUE
syntax. Note that the value is expected to be in decimal format and it has to
fit in a byte. Hence, you could have a code like follows:
.ifdef PAL
lda #1
.else
lda #0
.endif
If you compile the code with -D PAL=1, then the first branch will be taken
instead of the second one.
The .nasm/ directory
When you enable the --write-info flag, some files will be written into a
hidden .nasm/ directory. This directory will contain debug information that
can be later used by other tools. That being said, some of these files can also
be useful to programmers. They are as follows:
segments.txt: summary of the segments being used and how much they have been filled. Note that you need to also pass--statsto get this file.memory.txt: list of variables being used, expressed as ranges to account for reservation via theasan:reservespecial comment. Note that you need to also pass--asanto get this file. See more on the address sanitizer below.addresses.txt: list of addresses known by the assembler, expressed as ranges to account for blocks of code like proc's. You don't need any other flag for this file.
Language features
In contrast to ca65, there are some nasm-specific features. First of all,
nasm defines the __NASM__ variable by default, with an integer value of
1. This way, if you plan on using something nasm-specific, you can always do
something like:
.ifdef __NASM__
;; whatever
.endif
It's expected from code written targetting ca65 to "just work" on nasm. That
being said, if you use nasm there might be some specific features which are
missing in ca65.
Unused code
This assembler will issue a warning whenever it finds unreferenced variables or labels. Hence, if you have something like:
foo:
rts
;; code that never references 'foo'.
Then you will get:
warning: label 'foo' is unused (unused.s)
In the case for subroutines defined via .proc, this warning will actually be
an error, as nasm can rightly identify that this is dead code.
This check can be skipped by providing the --allow-unused flag on nasm.
Cross-mapping references
This assembler will issue a warning whenever you are referencing an object which is defined into a segment from another mapping. Some segments, like the 'vectors' one, will reference code that is outside of its mapping. But in some other configurations, segments cannot make these cross-mapping references so happily. Imagine that we have an UNROM chip configuration, where "SWAPPABLE" is a segment that can be swapped according to the specification of this mapper chip. Then, you could have code like this:
.segment "SWAPPABLE"
.proc foo
rts
.endproc
.segment "FIXED"
jsr foo
Here the assembler will properly detect the address of 'foo' in the context of the 'SWAPPABLE' segment. But what this assembler doesn't know is that this segment is swappable. Hence, if the bank being mapped right now is not the one containing the 'SWAPPABLE' segment, then the address computed for 'foo' and used in that 'jsr' instruction will point to something else entirely.
This is something that can only be inspected at runtime, and so the assembler
cannot be of much help here. Hence, nasm adds a warning so the programmer can
understand the potentially dangerous operation.
All of that being said, this assembler also adds support for "asan:safe" or "check:safe", which is a magic comment that the programmer can write to re-assure the assembler that this operation is fine (e.g. there is a guarantee that the mapped bank is that one we are expecting). Hence, the code above could now be written like so:
jsr foo ; check:safe
Moreover, you can define whole segments as "fixed" ones with the
asan:fixed-segments (or check:fixed-segments) comments. This way, you
express to the assembler that references to addresses of these segments are
guaranteed to always be valid. Consider the following example:
;;; asan:fixed-segments ONE, OTHER
.segment "ONE"
.proc foo
rts
.endproc
.segment "OTHER"
.proc bar
rts
.endproc
.segment "FIXED"
jsr foo
jsr bar
In the code above, we state that both 'ONE' and 'OTHER' are guaranteed to have a stable address space (they are fixed, never to be re-mapped). Hence, the assembler won't spit any warning at the final two 'jsr' instructions. Also note that you can define this comment multiple times. So the code below achieves the same thing:
;;; asan:fixed-segments ONE
.segment "ONE"
;; bla bla
;;; asan:fixed-segments OTHER
.segment "OTHER"
;; rest
Defining global labels
For optimization reasons, sometimes it's necessary to write a label that can be
accessed globally, regardless of the current scope. The
jetpac.nes game has a good example of
this need on its enemies.s file. In there, we define a function pointer that
is set to the current function handler for the enemies' algorithm. Then enemy
handling can go along like this:
;; previous code
lda #.hibyte(@return_from_movement_handler - 1)
pha
lda #.lobyte(@return_from_movement_handler - 1)
pha
jmp (zp_movement_fn)
@return_from_movement_handler:
;; Rest of the code after enemy movement has been handled.
That is, we push onto the stack the address of @return_from_movement_handler,
and then jmp to the function handler. Then, the function handler can simply
call rts and everything will be fine. As explained in the source code, other
techniques like trampolines or the "rts trick" were not desirable on this
context.
Moreover, note that a simple jmp was not possible from the context of enemy
handlers, as @return_from_movement_handler is inside of the scope of the
Enemies::update proc. For this game the performance penalty with this setup
was acceptable, but note that it's inside of a loop, and these extra cycles are
given for each enemy.
But this is potentially bad if your game is more tight on the cycle count and
every cycle you can squeeze matters. For this reason, nasm has the possibility
to define labels globally. That is, the above example could be rewritten like
this:
jmp (zp_movement_fn)
#@return_from_movement_handler:
;; Rest of the code after enemy movement has been handled.
Notice the leading '#' symbol. This tells nasm that
@return_from_movement_handler should be defined at the global scope,
regardless of the current one (hence, it's no longer hidden inside of the
Enemies::update scope). With this, then each handler can switch from an rts
to:
jmp @return_from_movement_handler ; or hide it on an 'RTS_FROM_ENEMY_HANDLER' macro.
In total, for each iteration loop, the setup would save 10 cycles (2 cycles x
lda, 3 cycles x pha), and replacing each handler's rts with a jmp would save
3 cycles (6 cycles 'rts' - 3 cycles 'jmp'). That is, given that this game
allowed 4 enemies at once, then each call to Enemies::update could save 13
cycles x 4 enemies = 52 cycles. Not a crazy amount, yes, but this optimization
is now so easy to pull that it's well worth it.
The __fallthrough__ pseudo-instruction
It's a well-known
optimization to avoid
jsr + rts chains, and that's why programmers usually change jsr with jmp
in cases such as this:
.proc foo
;; code
jmp bar
;; instead of:
;; jsr bar
;; rts
.endproc
;; comments, documentation, blank lines, whatever
.proc bar
;; code
rts
.endproc
But if you try to assemble this code by using nasm you will get the following
warning:
warning: unconditional jump that points to the next instruction (bad_fallthrough.s: line 11)
This is because foo and bar happen to be contiguous and nasm is telling
you that a further optimization can be done by removing the jmp instruction
altogether. This is "falling through" the code as it's done in high-level
programming languages such as C. Hence:
.proc foo
;; code
;; fall through 'bar'
.endproc
;; comments, documentation, blank lines, whatever
.proc bar
;; code
rts
.endproc
In these cases, when reading the code, a programmer might not be totally sure on
whether the code is missing an rts or a jmp instruction, or whether this
falling through was done on purpose. Hence, as it's done above, one idea is to
leave a comment for future reference.
But nasm also provides the __fallthrough__ pseudo-instruction, which allows
developers to explicitly specify that the code flow is expected to fall through
to the next instruction, even if the code layout might not make this obvious, or
it might seem an accident. Hence, the above could have been written like so:
.proc foo
;; code
__fallthrough__
.endproc
;; comments, documentation, blank lines, whatever
.proc bar
;; code
rts
.endproc
This is not that much different than writing a comment. But this pseudo-instruction also accepts an argument, which is the label that you expect the code to fall through. Hence, the previous code can be better written like this:
.proc foo
;; code
__fallthrough__ bar
.endproc
;; comments, documentation, blank lines, whatever
.proc bar
;; code
rts
.endproc
This is semantically similar, but we are also stating that we expect to fall to
the first instruction of bar. That is, if now the programmer moves bar
somewhere else, or puts some code in between both functions, then the assembler
will issue the following error:
error: statement expects to progress on 'bar' ($8004), but the next address is $8002 (bad_fallthrough.s: line 11)
This way, programmers can perform this optimization while also making sure that the assembler will catch the error whenever code moves around.
Last but not least, the --prelude flag provides a default implementation so
ca65 doesn't break on an otherwise unknown __fallthrough__ identifier. The
implementation looks like this:
.ifndef __NASM__
.macro __fallthrough__ arg
.endmacro
.endif
Mappers
By default nasm will assume the configuration for an NROM mapper, but this
can be changed with the -c/--configuration flag, with which you can pass the
path for the linker configuration file you'd like to use. This file can either
follow the same ld65 syntax, or a
simplified one (check out some samples for the "simplified" syntax
here).
All of that being said, and out of convenience, this flag also accepts this set
of values: empty, nrom, nrom65, unrom, uxrom and mmc1. These
values correspond to the configurations already
bundled on this application.
Address sanitizer
This assembler comes with a set of tools that builds up an "address sanitizer". Some of its functionality is already included, while some other is opt-in via a special command line option.
Reserved memory
This assembler can detect the memory regions being used and make decisions out
of it. This comes with a few gotchas that the programmer has to be aware in
order for the assembler to be useful. Because of this, the tooling around this
detection is behind the -a/--asan flag.
The address sanitizer will blindly follow the naming conventions from style.nes, and assume at first that each variable takes 1 byte exactly. Hence, in order to reserve one byte, you can simply:
zp_variable = $20
Then the address sanitizer will assume that you are reserving a byte at address
$20 which will be used throughout the code. If you want to reserve more than a
byte, then:
zp_buffer = $20 ; asan:reserve $0F
Then the address sanitizer will assume that zp_buffer = $20..$2F
(included). Any access to this reserved range that is not strictly via
zp_buffer will be considered a conflict. For example:
zp_buffer = $20 ; asan:reserve $0F
zp_bad = $22 ; NOTE: The address sanitizer will mark it as a *conflict*.
If you want to ignore this (e.g. you are using a variable that shadows other ones), then you can explicitely tell the address sanitizer to ignore a given assignment or instruction:
zp_buffer = $20 ; asan:reserve $0F
zp_good = $22 ; asan:ignore
lda $200 ; asan:ignore
That is, in order for the address sanitizer to be successful, it will also warn programmers whenever an access to memory is being done without using variables. This allows for the address sanitizer to be more thorough, even if there is never the guarantee that memory accesses will be safe. For example:
zp_variable = $20
ldx #20
lda zp_variable, x
This will access memory far beyond to zp_variable which was only reserving a
single byte. This is beyond the scope of this tool and other tools should be
used instead (e.g. an emulator with breakpoints on accesses to unexpected memory
regions, or runrom from this project).
On another note, the address sanitizer is also able to do some basic bound checks. For example:
zp_variable = $20 ; asan:reserve $02
lda zp_variable ; good
lda zp_variable + 1 ; good: arithmetics within the reserved limits.
lda zp_variable + 2 ; bad: arithmetics that would point to out of bounds.
lda zp_variable - 1 ; bad: same as before but wrapping around.
Last but not least, what if we want to reserve some space which is not exactly assigned to a variable? This happens basically with the stack, and for this you can use the following statement:
;; asan:stack $0100-$01FF
Only one of these statements is allowed for a project. You can explicitely
indicate that we are on the $0100 page as above, or you can especify just a
byte:
;; asan:stack $00-$FF
The above will implicitely assume we are on the $0100 page. But, for this
simple case, a shorthand is given when you want to use the whole page:
;; asan:stack full
With all of this, nasm will be able to tell how much memory you've been using
so far, and if you add the --write-info option, you will also know how this
memory is laid out by reading the .nasm/memory.txt file. Couple that with the
--stats option, and for a given project you will be able to know:
- How much do you have left in memory and in your ROM segments.
- You have no conflicting variables or variables that shadow others in unexpected ways.
- Where exactly you are reserving RAM and ROM space.
- Basic arithmetics don't make you fall out of bounds.
Working RAM
In NES/Famicom programs you have to advertise on the header whether Working RAM
is being used or not (see byte 6 on the iNES
format). The programmer is expected to set
this flag on if PRG RAM is available. If there are memory accesses to PRG RAM
but the programmer did not advertise that on the header flag, then nasm will
error out as it cannot assume that these accesses are valid. This check will
happen regardless of the -a/--asan flag.
