4
\$\begingroup\$

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.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.