Jumping into ARM assembly


Linux-SWAT

Forum Addict!
Joined
Feb 13, 2010
Messages
9,188
Hi !

I finally decided myself to practice a bit of ARM assembly.
I already coded a bit of asm using the HP-48's Saturn CPU and I began to read some ARM docs.

I would like to know how to natively run some asm on the Pandora, beginning with some ADD and memory operations, but I'm not sure where to begin with compiler/tools.
I'm a visual guy, I need to see register states and use step by step execution.

My first long-term objective is to write a black and white tron game, as I did on HP-48.
 
You're in luck as every pandora already comes equipped with asm programming tools (binutils).

Here is a small example to get you started, "hello.s":

Code:
.text
.align 2

.global _start
_start:
  mov r0, #1    @ file descriptor 1 - stdout
  ldr r1, =text @ buffer to write
  mov r2, #13   @ buffer length
  mov r7, #4    @ system call number, 4 is 'write'
  swi 0         @ call the Linux kernel

  mov r7, #1    @ exit
  swi 0

.data
text: .asciz "hello world!\n"
Compile, link and run:
Code:
$ as hello.s -o hello.o
$ ld -o hello hello.o
$ ./hello
hello world!
For stepping, just use gdb:
Code:
# gdb ./hello
...
Reading symbols from /home/notaz/stuff/asm/hello...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x8074
(gdb) run
Starting program: /home/notaz/stuff/asm/hello

Breakpoint 1, 0x00008074 in _start ()

(gdb) x/7i _start
=> 0x8074 <_start>:    mov    r0, #1
   0x8078 <_start+4>:    ldr    r1, [pc, #16]    ; 0x8090 <_start+28>
   0x807c <_start+8>:    mov    r2, #13
   0x8080 <_start+12>:    mov    r7, #4
   0x8084 <_start+16>:    svc    0x00000000
   0x8088 <_start+20>:    mov    r7, #1
   0x808c <_start+24>:    svc    0x00000000
(gdb) si
0x00008078 in _start ()
(gdb) x/7i _start
   0x8074 <_start>:    mov    r0, #1
=> 0x8078 <_start+4>:    ldr    r1, [pc, #16]    ; 0x8090 <_start+28>
   0x807c <_start+8>:    mov    r2, #13
   0x8080 <_start+12>:    mov    r7, #4
   0x8084 <_start+16>:    svc    0x00000000
   0x8088 <_start+20>:    mov    r7, #1
   0x808c <_start+24>:    svc    0x00000000
You can use "info registers" to see all the registers and/or use any other feature of gdb.
 
 
Last edited by a moderator:
Thanks for the example, I've also toyed with the idea of jumping into ARM assembly for sometime. 
 
If you want to use Assembler inside C code I would suggest to use "asm volatile" like this. I use a bit asm code in sparrow3d to fill a big memory chunk with the same int using stmia on arm devices.
The first parameter is the string containing your asm code. The second parameter are output variables, which are mapped to registers and the third parameter input variables, which are used to init registers. The last parameter tells gcc, which registers you will touch in your code (so which should not be used anymore).
The output and input registers are accesable via %0, %1, %2 usw. Read more here.
 
Interesting that you need to call system call 7 to quit the program - on older ARM systems I've used you just let PC exceed the end of the program and the OS would tidy it up for you; for that reason you generally have a label after any data to perform any manual cleanup before it quits.
 
Interesting that you need to call system call 7 to quit the program - on older ARM systems I've used you just let PC exceed the end of the program and the OS would tidy it up for you; for that reason you generally have a label after any data to perform any manual cleanup before it quits.

I guess you linked the C library or libgcc (done automatically if you compile with gcc instead of as+ld like I showed above), which added init and cleanup code for you, and your program would run into the next function after your's which would eventually return and that would take you to the cleanup code.

My example has absolutely no other code, it would run out to the other section of ELF file that's loaded in memory and crash. There is no way out, you have to explicitly ask the kernel to stop the process.
 
Have many questions, but for now, I just wanted to print "Hello World" twice :
 

Code:
        .text
        .align 2

        .global _start
_start:
        mov r0, #1    @ file descriptor 1 - stdout
        ldr r1, =text @ buffer to write
        mov r2, #13   @ buffer length
        mov r7, #4    @ system call number, 4 is 'write'
        swi 0         @ call the Linux kernel

        mov r0, #1    @ file descriptor 1 - stdout
        swi 0         @ call the Linux kernel

        mov r7, #1    @ exit
        swi 0

        .data
text:    .asciz "hello world!\n"
Is it the right way to do it ?

 
 
I haven't seen the documentation for system call 4, so it's possible r1, r2 and r7 are modified by the call, so you'll need to reset those too.

Also, it's possible none of them are modified, so setting r0 again has no effect.  You could test to see if your registers have been modified, but really you should try to find the documentation of this call so you can be sure you're not relying on unspecified behaviour which may change in the future.

Also, your indentation is significantly different to the example notaz gave.  I'm not sure if that matters with the assembler you're using or not.

Also, from what I can tell in later revisions, the SWI mnemonic has been replaced by one called 'SVC'.  But as long as the assembler understands it, both will correspond to the correct machine code - it's just a name change, I think.
 
The way I did worked.
According to gdb, I had to "mov r0, #1" as the register was reset by the first call.

Meanwhile I'm looking a way to do a loop like while i != 0 then print "hello world", i-- .
 
Last edited by a moderator:
Here's my little loop ! Feel free to correct/comment if something's not ok there :
 

Code:
        .text
        .align 2

        .global _start
_start:

        ldr r1, =text @ buffer to write
        mov r2, #13   @ buffer length
        mov r7, #4    @ system call number, 4 is 'write'

        mov r5, #0    @	set the counter to zero
_loop:

        mov r0, #1    @ file descriptor 1 - stdout
        svc 0         @ call the Linux kernel and print	the text

        add r5, #1    @	increment the counter
        cmp r5, #5    @ compare if the counter == 5
        bne _loop     @	if not,	come back to _loop

        mov r7, #1    @ exit
        svc 0


        .data
text:    .asciz "hello world!\n"
 
Last edited by a moderator:
According to gdb, I had to "mov r0, #1" as the register was reset by the first call.

Yup you get return code there, which is how many bytes were actually written, or negative error code for error.

Also, your indentation is significantly different to the example notaz gave.  I'm not sure if that matters with the assembler you're using or not.

It doesn't.

Also, from what I can tell in later revisions, the SWI mnemonic has been replaced by one called 'SVC'.  But as long as the assembler understands it, both will correspond to the correct machine code - it's just a name change, I think.

Right, it's my old habit from GP2X times.

BTW for system call numbers, it's easiest to look at the kernel's source:
http://git.openpandora.org/cgi-bin/gitweb.cgi?p=pandora-kernel.git;a=blob;f=arch/arm/include/asm/unistd.h;h=4a1123783806b79ef559ed51a093353feb67070e;hb=HEAD
As for documentation, it's in standard Linux manpages, like "man 2 write" (have to install manpages-dev package or similar, not available on pandora though). First argument is r0, second in r1 and so on, and syscall number in r7.
 
Last edited by a moderator:
--Edited : this code reads a character on the command line, then displays it

Code:
        .text
        .align 2

        .global _start
_start:

        mov r0, #0    @ file descriptor 0 - stdin
        mov r7, #3    @ system call number, 3 is 'read'
        mov r2, #1    @ buffer length
        ldr r1, =input
        svc 0

        mov r0, #1    @ file descriptor 1 - stdout
        mov r7, #4    @ system call number, 4 is 'write'
        svc 0         @ call the Linux kernel and print the text

        mov r7, #1    @ exit
        svc 0


        .data
input:   .asciz "%d"
 
Last edited by a moderator:
You forgot to do svc to call the kernel to do the read, and use "input: .byte 0", format strings like "%d" are for C library functions, kernel does not support those.
 
Last edited by a moderator:
It was working with %d. Anyway, I applied your change, here's the updated code.

So, read a character from the CLI, then displays it :

Code:
        .text
        .align 2

        .global _start
_start:

        mov r0, #0    @ file descriptor 0 - stdin
        mov r7, #3    @ system call number, 3 is 'read'
        mov r2, #1    @ buffer length
        ldr r1, =input @ will load the input into r1
	svc 0              @ call the Linux kernel and wait for the input

        mov r0, #1    @ file descriptor 1 - stdout
        mov r7, #4    @ system call number, 4 is 'write'
        svc 0         @ call the Linux kernel and print the text

        mov r7, #1    @ exit
        svc 0


        .data
input:  .byte 0
 
Last edited by a moderator:
I was working on a 5432 board, maybe that's why it worked.

Ok, now, how do I read presses on the Pandora D-pad ?
 
Last edited by a moderator:
Here's my little loop ! Feel free to correct/comment if something's not ok there :

Looks fine.  Just a note that it's always better to count down in loops than to count up, because you can test for zero without doing a direct CMP - the EQ conditional tests for zero after a SUBS (subtract with status flags):

MOV R5, #5
_loop:
@ Do stuff
SUBS R5, #1
BNE _loop @ Branch if not zero
_endloop:

(at least I think it's the EQ/NE conditionals you need after an ADDS/SUBS, but it's been a while)
As for documentation, it's in standard Linux manpages, like "man 2 write" (have to install manpages-dev package or similar, not available on pandora though). First argument is r0, second in r1 and so on, and syscall number in r7.

LinuxSWAT figured out that R1 and R2 are either restored or simply not touched after a SVC #0 call, but unless I see it documented somewhere that's that's by design, not coincidence, I'd be tempted to set all of the relevant registers each time to be safe.

Edit: And while I'm asking, where can you find out what r7 numbers correspond to which manpage?
 
Last edited by a moderator:
Ok, now, how do I read presses on the Pandora D-pad ?

It's a bit complicated, you have to open /dev/input/event4 and read the file descriptor you get, then in the memory you read at certain offset there will be a keycode and event value where 1 means key down, 0 key up. The memory is described by this C structure:
http://git.openpandora.org/cgi-bin/gitweb.cgi?p=pandora-kernel.git;a=blob;f=include/linux/input.h;h=3862e32c4eeb38076d4768c70a958a7b58dd6cf8;hb=HEAD#l26
Alternatively you can use some library like SDL, but then it's even more work when you do it from asm.

LinuxSWAT figured out that R1 and R2 are either restored or simply not touched after a SVC #0 call, but unless I see it documented somewhere that's that's by design, not coincidence, I'd be tempted to set all of the relevant registers each time to be safe.

I think it should be safe to rely on it not changing the registers as it has no need to do it, the kernel has it's own register state and stack.

Edit: And while I'm asking, where can you find out what r7 numbers correspond to which manpage?

See the link in post 12 of this thread.
 
Last edited by a moderator:
Ah, thanks, missed that.

As far as I can tell, no processor mode banks R0-R7.  R8-R12 are only banked in FIQ mode.  SVC mode gets a new stack pointer and link register, but you need what's in the latter IIRC.  You can just STM all the registers onto a stack and LDM them back out at the end - but that costs cycles, and on ARM's old RISC OS, some specified registers were generally trashed after an SWI call (depending on which call it was you made), but I guess that's the difference between a hokey old microcomputer OS and a server OS.
 
Hello,
I've written and posted here a full game written in pure ASM language. Only syscalls and /dev/input/event reads are used.
Feel free to look at the sources.

One advice, use macros. They make your coding life much much easier. Like this ones:

to increment a register's value:

Code:
.macro inc rreg
 add \rreg,\rreg,#1
.endm
usage:
Code:
 inc r5

[FONT=sans-serif]to do simple loops:[/FONT]
Code:
.macro regloop rreg,oofs
 subs \rreg,\rreg,#1
 bne \oofs
.endm 

.macro print str
 mov r0,#console
 adr r1,1f
 mov r2,#3f-1f
 b 2f
1: .ascii "\str"
3: .align
2: invoke sys_write
.endm 
usage:
Code:
 mov r5,#5
1:
 print "hello\n"
regloop r5,1b

and so on. I included a few into the macro.mac file in the archive.

Happy coding!
 
 
Last edited by a moderator:
Back
Top