carminline-assemblyarmv7

ARM V7 inline assembly - moving a C variable into a register


There are no questions on ARM V7 inline assembly that answer mine so I made a post. I want to move the value of C variables into r0-r2 and the other way round - from the registers into C variables. However, my code always loads the same value - 12 - into the register. Can anyone tell me how I can achieve my goal?

// syscalls.h
#ifndef SYSCALL_H
#define SYSCALL_H
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define SYSCALL_ID_EXIT          1
#define SYSCALL_ID_CREATE_THREAD 2
#define SYSCALL_ID_GETC          3
#define SYSCALL_ID_PUTC          4
#define SYSCALL_ID_SLEEP         5
#define SYSCALL_ID_UNDEFINED     6
#ifndef __ASSEMBLER__
//...
// syscalls.c
// ...
void syscall_create_thread(void (*f) (void *), void * args, unsigned int arg_size){
    unsigned int func = (unsigned int) f;
    unsigned int arguments = (unsigned int) args;
    unsigned int argument_size = (unsigned int) arg_size;
    asm("mov r0, %0" : "=r"(func)::"r0");
    asm("mov r1, %0" : "=r"(arguments)::"r1");
    asm("mov r2, %0" : "=r"(argument_size)::"r2");
    asm("mov r7, #" TOSTRING(SYSCALL_ID_CREATE_THREAD));
    asm("SVC #0");
}

Solution

  • Your specific use-case can be done as simple as following:

    __attribute__((naked))
    void syscall_create_thread(void (*f) (void *), void * args, unsigned int arg_size) 
    {
        asm("mov r7, #" TOSTRING(SYSCALL_ID_CREATE_THREAD) :::"r7");
        asm("SVC #0");
        asm("RET");
    }
    

    This relies on the fact that the calling convention for your architecture defines that the arguments being passed to a function are placed in the registers r0, r1... by the caller. When you make the function naked you are telling the compiler not to generate any prologue/epilogue for the function, that is not to create a stack frame, save the callee-saved registers or even add ret instruction. But it means that you need to be extra careful too and add the missing pieces yourself.

    Here is the final example code and the generated assembly to convince you that it works:

    #include <stdlib.h>
    #define STRINGIFY(x) #x
    #define TOSTRING(x) STRINGIFY(x)
    #define SYSCALL_ID_CREATE_THREAD 2
    
    __attribute__((naked))
    void syscall_create_thread(void (*f) (void *), void * args, unsigned int arg_size) 
    {
        asm("mov r7, #" TOSTRING(SYSCALL_ID_CREATE_THREAD) :::"r7");
        asm("SVC #0");
        asm("RET");
    }
    
    
    void callback(void* args)
    {
        return;
    }
    
    void caller() {
        int arg = 8;
        syscall_create_thread(callback, &arg, sizeof (int));
    }
    

    And the generated assembly (annotated by me):

    callback:
            bx      lr
    syscall_create_thread:
            mov r7, #2
            SVC #0
            RET
    caller:
            str     lr, [sp, #-4]!
            sub     sp, sp, #12
            mov     r3, #8
            str     r3, [sp, #4]
            mov     r2, #4              // r2=sizeof(int)
            add     r1, sp, r2          // r1 = address of the local `arg` variable
            ldr     r0, .L5             // r0 = address of callback function
            bl      syscall_create_thread
            add     sp, sp, #12
            ldr     lr, [sp], #4
            bx      lr
    .L5:
            .word   callback
    

    See this on Godbolt

    EDIT: Apparently, when the function is "naked", there is no effect of the r7 being marked as clobbered, so it won't be "callee-saved" as it should, so some amendment is needed for the code above to actually make sure r7 value is saved and restored within the syscall_create_thread() function. The simplest would be to do so in one of the non-callee-saved registers, such as r3 or r4. Or it can be done on stack.