Dabbling a bit more with arm assembly, I wrote a few functions to handle string splitting. The goal was to write a few reusable functions that could be combined to emulate (roughly) what C strtok()
does. The approach was essentially to write strspn()
and strcspn()
approximations and then combine them in the function that tokenizes the string.
All works as intended, but I'm concerned about the efficiency of the data-handling. The loads, moves, adds, and register use seem like it should be able to be improved on. Currently it is written so it was easy to read, but each function is making use of up to five registers (r4
- r8
) as well as grabbing an extra few bytes of stack space when needed. Is there a trade-off in pushing and popping that number of registers with every function call compared with just using additional stack space in each function for temporary storage?
The primary feedback I'm looking for is whether the data-handling between and within the functions is reasonable and makes sense, or if there are glaring areas where that can be optimized.
The main program prompts for and takes the string to tokenize along with the delimiter string as user inputs. It then loops calling strtoka
keeping track of the number of tokens found and the number of characters consumed in the string each iteration. The token number along with the token are output. The program is limited to processing 10 tokens just due to the output using a single digit token-number for testing. Otherwise it will handle any number of tokens up to a 32-bit limit.
(all the string function names just have an a
tacked onto the end of the C function names, e.g. strtoka
, strspna
, strcspna
, strlena
, ...)
The example code is:
/* Split string on delimiters
* Reads string and delimiters as user input and splits string on
* occurrence or sequence of deliters entered. Roughly modeling
* C strtok() implemented with alternating strspn() and strcspn().
*
* Compile/Link:
* as -mcpu=cortex-a53 -o obj/split_string.o split_string.s
* ld -o bin/split_string obj/split_string.o
*
* Example Use:
*
* enter string : -.one,two.three-four+five?six
* delimiters : ?+-.,
* token 0 : one
* token 1 : two
* token 2 : three
* token 3 : four
* token 4 : five
* token 5 : six
*
*/
/* Define Raspberry Pi 3 B/B+
*/
.cpu cortex-a53
.fpu neon-fp-armv8
.syntax unified /* modern syntax */
/* constants uninitialized variables
*/
.bss
.align 2
.lcomm delim, 32
.lcomm buffer, 128
/* constannts for functions
*/
.data
.align 2
.equ STDIN, 0
.equ STDOUT, 1
.equ STDERR, 2
.equ MAXD, 32
.equ MAXC, 128
.equ nul, 0
prompt: .asciz "enter string : "
.equ promptlen,.-prompt
spliton: .asciz "delimiters : "
.equ splitonlen,.-spliton
result: .asciz "token "
.equ resultlen,.-result
resend: .asciz " : "
.equ resendlen,.-resend
.text
.align 2
.global _start
.type _start, %function
_start:
stmdb sp!, {r4, r5, r6, r7, r8, fp, lr} /* prologue */
mov fp, sp /* set stack frame */
sub sp, sp, 8 /* 4-bytes extra storage */
eor r4, r4, r4 /* zero token counter */
eor r7, r7, r7 /* zero offset in string */
/* prompt, read and validate string to be split */
mov r0, STDOUT /* stdout file no. to r0 */
ldr r1, promptadr /* prompt address in r1 */
mov r2, promptlen /* length in r2 */
bl writenchar /* display prompt */
mov r0, STDIN /* stdin file no. in r0 */
ldr r1, bufferadr /* char array address in r1 */
mov r2, MAXC /* length in r2 */
bl readstr /* read user-input */
cmp r0, 0 /* VALIDATE no. of chars read */
moveq r1, 1 /* return EXIT_FAILURE */
beq returnerr /* branch to error */
/* prompt, read and validate delimiters to split string */
mov r0, STDOUT /* stdout file no. to r0 */
ldr r1, splitonadr /* result address in r1 */
mov r2, splitonlen /* length in r2 */
bl writenchar /* display result prefix */
mov r0, STDIN /* stdin file no. in r0 */
ldr r1, delimadr /* char array address in r1 */
mov r2, MAXD /* length in r2 */
bl readstr /* read user-input */
cmp r0, 0 /* VALIDATE no. of chars read */
beq returnerr /* branch to error */
tokenize:
/* call strcspna to obtain length of first token */
ldr r0, bufferadr /* load buffer addr in r0 */
add r0, r0, r7
ldr r1, delimadr /* load delimiter addr in r1 */
bl strtoka /* strtok() */
cmp r2, 0 /* check token length */
beq tokend /* done if zero */
sub r3, r1, r0 /* get delimiters consumed */
add r3, r3, r2 /* get total offset for tok */
add r7, r7, r3 /* add to offset from str start */
mov r6, r1 /* save token start addr */
mov r8, r2 /* save no. chars in token */
/* output prefix for token */
mov r0, STDOUT /* stdout file no. to r0 */
ldr r1, resultadr /* result address in r1 */
mov r2, resultlen /* length in r2 */
bl writenchar /* display result prefix */
add r5, r4, '0' /* add ASCII '0' to token count */
strb r5, [sp, -1]! /* token no. at sp, pre-increment */
add r4, r4, 1 /* increment token count */
mov r0, STDOUT /* stdout file no. to r0 */
mov r1, sp /* char address in r2 */
mov r2, 1 /* length in r2 */
bl writenchar
mov r0, STDOUT /* stdout file no. to r0 */
ldr r1, resendadr /* result address in r1 */
mov r2, resendlen /* length in r2 */
bl writenchar /* display result ending */
mov r0, STDOUT /* display token */
mov r1, r6
mov r2, r8
bl writenchar
bl newln /* tidy up with newline */
mov r0, 10 /* check count < 10 */
cmp r4, r0 /* (single digit output limit) */
beq tokend
b tokenize
tokend:
mov r0, 0 /* return EXIT_SUCCESS */
b alldone
returnerr:
mov r0, 1
alldone:
add sp, sp, 8
ldmia sp!, {r4, r5, r6, r7, r8, fp, lr} /* epilogue */
mov r7, 1 /* __NR_exit syscall */
svc 0
bufferadr: .word buffer
delimadr: .word delim
promptadr: .word prompt
splitonadr: .word spliton
resultadr: .word result
resendadr: .word resend
/* implements strspn from C. finds the initial number of
* characters in str (r0) that are contained in delim.
*
* parameters:
* r0 - address of string
* r1 - address of delimiter string
* return:
* r0 - number of initial characters matching chars in delimiter
*/
.text
.align 2
.global strspna
.type strspna, %function
strspna:
stmda sp!, {r4, r5, r6, r8, fp, lr} /* prologue */
sub fp, sp, 4 /* set stack frame */
eor r4, r4, r4 /* zero r4 for counter */
strspstr:
ldrb r5, [r0, r4] /* load str char in r5 w/offset r4 */
cmp r5, nul /* check for nul-char end of str */
beq strspnex
mov r6, r1 /* load delim str address in r6 */
strspdelim:
ldrb r8, [r6], 1 /* load delim char in r8 post inc r6 */
cmp r8, nul /* check end of delim */
beq strspnex /* delim not found, done */
cmp r8, r5 /* cmp delim char & str char */
addeq r4, 1 /* inc no. offset in str */
beq strspstr /* if chars eq, get next str char */
b strspdelim /* get next char in delim */
strspnex:
mov r0, r4 /* return initial no. chars in r0 */
ldmib sp!, {r4, r5, r6, r8, fp, lr} /* epilogue */
bx lr /* return */
/* implements strcspn from C. finds the initial number of
* characters in str (r0) that are not contained in delim.
*
* parameters:
* r0 - address of string
* r1 - address of delimiter string
* return:
* r0 - number of initial characters not in delimiter
*/
.text
.align 2
.global strcspna
.type strcspna, %function
strcspna:
stmda sp!, {r4, r5, r6, r8, fp, lr} /* prologue */
sub fp, sp, 4 /* set stack frame */
eor r4, r4, r4 /* zero r4 for counter */
strcstr:
ldrb r5, [r0, r4] /* load str char in r5 w/offset r4 */
cmp r5, nul /* check for nul-char end of str */
beq strcspnex
mov r6, r1 /* load delim str address in r6 */
strcdelim:
ldrb r8, [r6], 1 /* load delim char in r8 post inc r6 */
cmp r8, nul /* check end of delim */
addeq r4, 1 /* inc no. offset in str */
beq strcstr /* get next char in str */
cmp r8, r5 /* cmp delim char & str char */
beq strcspnex /* if chars eq, done */
b strcdelim /* get next char in delim */
strcspnex:
mov r0, r4 /* return initial no. chars in r0 */
ldmib sp!, {r4, r5, r6, r8, fp, lr} /* epilogue */
bx lr /* return */
/* implements strtok from C. locates start of token in string
* delimiter string returing address of token in r1 and length
* in r2.
*
* parameters:
* r0 - address of string (updated in caller after each token)
* r1 - address of delimiter string
* return:
* r0 - address (unchanged)
* r1 - address of start of token
* r2 - number of characers in token
*/
.text
.align 2
.global strtoka
.type strtoka, %function
strtoka:
stmda sp!, {r4, r5, r6, r8, fp, lr} /* prologue */
sub fp, sp, 4 /* set stack frame */
mov r5, r0 /* save string address in r5 */
mov r8, r1 /* save delimiter address in r8 */
bl strspna /* handle leading delimiters */
mov r4, r0 /* save offset to first token in r4 */
ldrb r6, [r5, r4] /* load char at r5 + r4 in r6 */
cmp r6, nul /* check at end/empty-string */
eoreq r2, r2, r2 /* return 0 chars in token in r2 */
beq strtokex /* branch to end */
add r6, r5, r4 /* add string addr and offset */
mov r0, r6 /* addr for token start in r0 */
mov r1, r8 /* set delimiter in r1 */
bl strcspna /* get token length */
mov r2, r0 /* return no. chars in token in r2 */
strtokex:
mov r0, r5 /* return addr of string in r0 */
mov r1, r6 /* return start of token in r1 */
ldmib sp!, {r4, r5, r6, r8, fp, lr} /* epilogue */
bx lr /* return */
/* read string from file descriptor upto max chars,
* '\n' character is read but not included in string
* parameters:
* r0 - file descriptor to read
* r1 - address of string to fill
* r2 - maximum number of bytes including null
* return:
* r1 - address (unchanged)
* r0, r2 - number of characters read
*/
.text
.align 2
.global readstr
.type readstr, %function
readstr:
stmdb sp!, {r4, r5, r6, r7, r8, fp, lr}
mov fp, sp
sub sp, sp, 4
mov r4, r0 /* file descriptor in r4 */
mov r5, r1 /* string address in r5 */
mov r6, r2 /* max count in r6 */
sub r6, r6, 1 /* subtract one for nul char */
eor r8, r8, r8 /* zero r8 for character count */
rdcharcnt:
mov r0, r4
add r1, r5, r8 /* load address + r8 into r1 */
mov r2, 1 /* read one byte */
mov r7, 3 /* __NR_read syscall */
svc 0
cmp r0, 1 /* validate 1-char read */
blt terminate /* on EOF or error, terminate */
@ mov r2, 10
ldrb r3, [r1] /* load character written in r3 */
cmp r3, 10 /* compare if \n character */
beq terminate /* terminate overwriting \n with \0 */
cmp r8, r6 /* max chars reached? */
beq rdcharcnt /* loop discarding additional chars */
add r8, 1 /* increment count in r2 */
b rdcharcnt /* loop, read next char */
terminate:
mov r0, 0 /* 0 for \0 in r0 */
@ strb r0, [r1]
strb r0, [r5, r8] /* stored \0 index in r8 */
mov r2, r8 /* set length return in r2 and r0 */
mov r0, r8
mov r1, r5 /* restore address of string in r1 */
readstrex: /* epilogue */
add sp, sp, 4
ldmia sp!, {r4, r5, r6, r7, r8, fp, lr}
bx lr /* return */
/* length of c-string strlen()
* r1 - address of string to write
* return:
* r2 - number of characters
*/
.text
.align 2
.global strlena
.type strlena, %function
strlena:
stmdb sp!, {r4, fp, lr} /* save r4, fp, lr */
mov fp, sp /* set stack frame */
sub sp, sp, 4 /* 8-byte align stack */
mov r4, r1 /* string address in r4 */
eor r2, r2 /* zero r2 for character count */
charcnt:
ldrb r3, [r4], 1 /* load char into r3, post-increment in r4 */
cmp r3, nul /* nul-terminating character? */
beq strlenex /* branch to write to file */
add r2, 1 /* increment count in r2 */
b charcnt /* loop */
strlenex:
add sp, sp, 4
ldmia sp!, {r4, fp, lr} /* epilogue */
bx lr /* return */
/* print c-string to file descriptor (stdout default)
* r0 - file descriptor
* r1 - address of string to write
* return:
* r0 - number of characters written
*/
.text
.align 2
.global writestr
.type writestr, %function
writestr:
stmdb sp!, {r7, fp, lr} /* save r7, fp, lr */
mov fp, sp /* set stack frame */
sub sp, sp, 4 /* 8-byte align stack */
bl strlena /* strlen of r1, length returned in r2 */
cmp r2, 0
beq wstrexit
mov r7, 4 /* __NR_write in r7 */
svc 0
wstrexit:
add sp, sp, 4
ldmia sp!, {r7, fp, lr} /* epilogue */
bx lr /* return */
/* print n-chars to file descriptor (stdout default)
* r0 - file descriptor
* r1 - address of string to write
* r2 - number of characters to write
* return:
* r0 - number of characters written
*/
.text
.align 2
.global writenchar
.type writenchar, %function
writenchar:
stmdb sp!, {r7, fp, lr} /* save r7, fp, lr */
mov fp, sp /* set stack frame */
sub sp, sp, 4 /* 8-byte align stack */
ldrb r7, [r1] /* load 1st char for comparison */
cmp r7, 0 /* check for empty-string */
beq wnchrexit
mov r7, 4 /* __NR_write in r7 */
svc 0
wnchrexit:
add sp, sp, 4
ldmia sp!, {r7, fp, lr} /* epilogue */
bx lr /* return */
/* write a newline to stdout
* no parameters, all registers preserved
* return:
* none
*/
.text
.align 2
.global newln
.type newln, %function
newln:
stmdb sp!, {r7, fp, lr} /* save r7, fp, lr */
mov fp, sp /* set stack frame */
sub sp, sp, 4 /* 8-byte align stack */
mov r0, 1 /* mov #1 to r0 (stdout fd) */
@ add r1, r1, sp /* sp address in r1 */
add r1, sp, 0 /* sp address in r1 */
mov r3, 0xa /* move '\n' to r3 */
mov r2, r0 /* move length of write to r2 */
strb r3, [r1, -1]! /* store '\n' at sp, pre-increment to write */
@ strb r3, [r1] /* store '\n' at sp, pre-increment to write */
mov r7, 4 /* __NR_write in r0 */
svc 0
add sp, sp, 4
ldmia sp!, {r7, fp, lr} /* epilogue */
bx lr /* return */
Also of interest is whether it is poor form just to use what seems like a large number of registers in each function instead of simply moving the stack pointer and using the stack space for storage instead. Situations like using two registers to temporarily save the token start address and number of characters in the token, e.g.
mov r6, r1 /* save token start addr */
mov r8, r2 /* save no. chars in token */
My thoughts were simply to use the registers available to avoid load or store later, but I end up saving and restoring 4-5 registers worth of data in each prologue and epilogue. That seems fine, but there may be considerations of efficiency I'm missing in the trade-off between pushing and popping the number of registers versus just using stack space within the function to minimize that.
Those are the primary areas of critique I'm interested in.