aboutsummaryrefslogtreecommitdiff

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 --stats to get this file.
  • memory.txt: list of variables being used, expressed as ranges to account for reservation via the asan:reserve special comment. Note that you need to also pass --asan to 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:

  1. How much do you have left in memory and in your ROM segments.
  2. You have no conflicting variables or variables that shadow others in unexpected ways.
  3. Where exactly you are reserving RAM and ROM space.
  4. 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.