ARM Assembler Question

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
ARM Assembler Question
by on (#171017)
(Assuming the Nintendo DS is "obsolete" so this thread can be in Other Retro Dev, move if need be).

I wanted to check out ARM assembly language, but don't have any money for a Raspberry Pi (my preferred method), so I decided to go with the Nintendo DS using libnds.

ARM makes sense to some extent, but due to problems with GAS not letting me grab just #defines from a file and ignore C code, if I continue with the NDS then C will be the primary language rather than the assembler.

That's not exactly what I'm wondering about, though. What I want to know is why GAS chooses to have registers "point to pointers" instead of be pointers.

As an example, for iprintf("HI!");, GAS would generate:
Code:
ldr r0, [message_pointer]
bl iprintf

message_pointer:
.word message

message:
.ascii "HI!\0"


The pointer isn't necessary, though, since afaik registers can be pointers for themselves:
Code:
ldr r0, =message
bl iprintf

message:
.ascii "HI!\0"


This is kind of a difficult question now that I think about it (and may or may not be kind of an excuse to have people to talk about ARM with) but does anyone know why GAS chooses to manually make pointers rather than just skip that step of the assembly process entirely? Is there a good reason, or is that just a random consequence of having a computer make code for you?
Re: ARM Assembler Question
by on (#171018)
ARM has an explicit instruction for "pull out the 32-bit pointer from this 16-bit offset in memory", but no "just load a 32-bit number directly into a register" (The latter is a pseudoinstruction that might string several individual instructions together, depending on the specific numeric value of the pointer)
Re: ARM Assembler Question
by on (#171019)
Probably because of the literal pool thing that ARM does. An immediate operand is limited to 8 bits but can be shifted, but pointers are 32 bits. There are two workarounds:
  1. The MIPS approach of constructing a constant in two steps with a load immediate shifted followed by an OR immediate. This works well on MIPS because constants can be 16 bits, with an optional 16-bit left shift only for load immediate (lui). On ARM, this can be effective for Game Boy Advance and Nintendo DS MMIO ports in 0x04000000-0x040003FC or 0x04800000-0x048003FC because they can be expressed as (0x40 << 20) | (regaddr << 2). But generic addresses aren't necessarily easy to build this way.
  2. Store the pointer in a table between one subroutine and the next and load it using PC-relative addressing. The assembler offers a ldr rxx, =whatever syntax to add the address to a nearby pool and generate a load instruction.
Re: ARM Assembler Question
by on (#171020)
Confusion ensues.

Quote:
An immediate operand is limited to 8 bits but can be shifted, but pointers are 32 bits.

Would this be an immediate operand? Because it loads a 32-bit number and assembles just fine: ldr r3, =0xffffffff
Re: ARM Assembler Question
by on (#171021)
Assemble the code, then disassemble (you can use e.g. objdump to disassemble an object file). You should see that your instruction has changed into a different form.
Re: ARM Assembler Question
by on (#171024)
nicklausw wrote:
Quote:
An immediate operand is limited to 8 bits but can be shifted, but pointers are 32 bits.

Would this be an immediate operand? Because it loads a 32-bit number and assembles just fine: ldr r3, =0xffffffff

The mvn instruction loads a number and EORs it with 0xFFFFFFFF it before storing it. For example, mvn r3, #0x000B puts 0xFFFFFFF4 into r3.

Because ldr rxx,=value is a macro, it can do any of several things. Usually it'll be assembled to mov, mvn, mov then orr, mvn then bic, or ldr from a constant pool. The last is most likely in Thumb.
Re: ARM Assembler Question
by on (#171028)
Oh! So the "double-pointers" take up less space and are faster then? Okay, I had it all backwards.

Is using immediate addressing that much of a slow-down, though, if at all? Because having to have a section of code just for pointers is not ideal.
Re: ARM Assembler Question
by on (#171029)
The literal pool takes two 32-bit memory cycles. (or one 16-bit plus one 32-bit in thumb mode)
Two in-line instructions take two 32-bit memory cycles (or ... some number of 16-bit fetches in thumb mode)

More to the point, if you try to prevent the compiler and/or assembler from using a literal pool, you're going to be fighting it the whole way.
Re: ARM Assembler Question
by on (#171030)
lidnariq wrote:
More to the point, if you try to prevent the compiler and/or assembler from using a literal pool, you're going to be fighting it the whole way.

Yeah, I'm not really trying to get the compiler to be evasive with that type of thing. If libnds functions will run like that, then I never have to look at that machine code mess, so it doesn't really concern me. It's just that I don't want to be redundant with assembler code written on my own.
Re: ARM Assembler Question
by on (#171032)
Premature optimization is a root of all kinds of evil. Get it working first, and then get it fast once it's working.
Re: ARM Assembler Question
by on (#171043)
This is a problem typical to RISC processors. The entire design is based on a word size, which is also the instruction size and register size. Additionally, ALL instructions are that word size, and there is no instructions that (directly) uses multiple words. As such, it is impossible to have a "load immediate into register" instruction, because this immediate already takes the whole word (in this case, 32-bit).

The only solution is to have a two-word instruction, but this is also impossible because RISC philosophy says all instructions should be 1 word long, supporting variable length instruction would make it much harder to pipeline the processor.

The solution used by ARM is to have the "second word of the instructions" (in this case, your pointer) stored not in the code, but right after the code. (this was an arbitrary decision, it could go before and work just as well). This turned out to be very practical for romhacking, as there is a pool of parameters used in a function right to eachother, which often makes you able to change things without even disassemble the routine :)

Note that RISC was never made to save memory (on the other hand, it wastes a lot of ROM as programs are stored very inefficiently - except for THUMB mode in ARM where it gets decent). It was made to simplify instruction decoding within the processor in order to get them to run faster.

Also note that there is NO pointer to your pointer. It is just an instruction relative to the program counter (r15). ARM assembly uses many pseudom-instructions, which acually are compiled to different instructions. Google "arm pseudo instruction" to get more details.

In this case
Code:
lda r0, =something


is probably equivalent to something like
Code:
here:
    lda r0, [r15], #something-here-8

The PC is always 2 words (8 bytes) ahead because of the pipeline.
Re: ARM Assembler Question
by on (#171046)
AFAIK Hitachi SuperH (used by the Saturn and Dreamcast) also uses literal pools.
Re: ARM Assembler Question
by on (#171074)
Bregalad wrote:
In this case
Code:
lda r0, =something


is probably equivalent to something like
Code:
here:
    lda r0, [r15], #something-here-8

The PC is always 2 words (8 bytes) ahead because of the pipeline.

Hell no, not only you fetch the wrong word, you'll corrupt the PC, or it will fault. This is post-indexed addressing, instead of regular offset addressing, which is the only form accepted for base addresses based on PC.
So it's more like:
Code:
    ldr r0, [pc, #off-8]

The -8 thing is true, PC is "ahead" because of pipeline. This is important to consider upon receiving imprecise faults (if I remember correctly!), the old PC points after the faulty instruction.
Re: ARM Assembler Question
by on (#171197)
Um...does anyone have any idea why lr might magically turn into pc in a subroutine? Because I have a problem where my subroutines will randomly turn into a bx lr loop sometimes, and I can't figure things out at all. Not sure what other information to provide.
Re: ARM Assembler Question
by on (#171199)
nicklausw wrote:
Um...does anyone have any idea why lr might magically turn into pc in a subroutine? Because I have a problem where my subroutines will randomly turn into a bx lr loop sometimes, and I can't figure things out at all. Not sure what other information to provide.

Update, I figured this one out on my own.

Putting:
Code:
stmfd  sp!, {lr}

at the beginning of subroutines, and:
Code:
ldmfd  sp!, {pc}

at the end prevents recursive lr's. Now to figure out what the crap "stmfd" and "ldmfd" mean.
Re: ARM Assembler Question
by on (#171203)
"Store multiple to full-descending stack" means "push", and "load multiple from full-descending stack" means "pull". It's the RISC principle: if they already have "load multiple" and "store multiple", why create new instructions just for the stack?
Re: ARM Assembler Question
by on (#171227)
nicklausw wrote:
nicklausw wrote:
Um...does anyone have any idea why lr might magically turn into pc in a subroutine? Because I have a problem where my subroutines will randomly turn into a bx lr loop sometimes, and I can't figure things out at all. Not sure what other information to provide.

Update, I figured this one out on my own.

Putting:
Code:
stmfd  sp!, {lr}

at the beginning of subroutines, and:
Code:
ldmfd  sp!, {pc}

at the end prevents recursive lr's. Now to figure out what the crap "stmfd" and "ldmfd" mean.


RISC architectures don't push the return address onto a RAM stack when a subroutine call occurs. They store it in a register instead (the "link register"). If a subroutine is a leaf subroutine it can just leave the return address there, but if it calls other subroutines then it needs to explicitly push the link register onto the stack.
Re: ARM Assembler Question
by on (#171504)
Jarhmander wrote:
Bregalad wrote:
In this case
Code:
lda r0, =something


is probably equivalent to something like
Code:
here:
    lda r0, [r15], #something-here-8

The PC is always 2 words (8 bytes) ahead because of the pipeline.

Hell no, not only you fetch the wrong word, you'll corrupt the PC, or it will fault. This is post-indexed addressing, instead of regular offset addressing, which is the only form accepted for base addresses based on PC.
So it's more like:
Code:
    ldr r0, [pc, #off-8]

The -8 thing is true, PC is "ahead" because of pipeline. This is important to consider upon receiving imprecise faults (if I remember correctly!), the old PC points after the faulty instruction.


How do you know the offset anyway?
Re: ARM Assembler Question
by on (#171505)
Ideally, the assembler computes the offset for you.
Re: ARM Assembler Question
by on (#171630)
psycopathicteen wrote:
How do you know the offset anyway?

tepples wrote:
Ideally, the assembler computes the offset for you.


When using the right instructions, you don't have to compute any offsets, the assembler does that job for you. For example, ldr r0, =const is transformed by the assembler for you to ldr r0, [pc, #magicoffset], like Bregalad said, where the magic offset is computed by the assembler so it loads a word after the subroutine from the so-called literal pool. When using labels, like any other assemblers, it does put the right offset in the instruction, just like in any 6502 assembler when you do things like ex: beq Label.