Search code examples
assemblylinux-kernelx86calling-convention

How does kernel_thread_helper pass parameters to kernel thread function using inline assembly?


I'm reading about the source code of linux kernel of version 2.6.11 and getting confused about the inline-assembly kernel_thread_helper in arch/i386/kernel/process.c.

Following is the assembly code. Initially, register %ebx stores the address of a function and register %edx stores a pointer which is going to be passed to the function.

extern void kernel_thread_helper(void);
__asm__(".section .text\n"
    ".align 4\n"
    "kernel_thread_helper:\n\t"
    "movl %edx,%eax\n\t"
    "pushl %edx\n\t"
    "call *%ebx\n\t"
    "pushl %eax\n\t"
    "call do_exit\n"
    ".previous");

I'm wondering:

  • Why does this piece of code execute movl %edx,%eax? Since pushl %edx has pushed the value of %edx in stack, the function pointed to by %ebx should then retrieve needed parameters from the stack successfully. So is there no need to copy %edx to %eax?

  • I know that when __attribute__((regparm(3))) is added in front of a function definition, the function will retrieve parameters in the order %eax, %edx, %ecx and stack. So I'm wondering, is the instruction copying the value in %edx to %eax designed to be compatible with functions that use this __attribute__? Function with such an attribute should retrieve the needed parameters from %eax and go on running as well! But this causes a problem: the previously pushed pointer by pushl %edx could remain in the stack if no one pops it off.

I'm new to Linux Kernel and maybe misunderstood something. I want to know how these assembly instructions work actually and what's wrong with my guess.


Solution

  • A comment in an earlier Linux kernel version (v2.5.0) explains this (the movl into eax was added in v2.1.131 to really be precise):

    int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
    {
        long retval, d0;
    
        __asm__ __volatile__(
            "movl %%esp,%%esi\n\t"
            "int $0x80\n\t"     /* Linux/i386 system call */
            "cmpl %%esp,%%esi\n\t"  /* child or parent? */
            "je 1f\n\t"     /* parent - jump */
            /* Load the argument into eax, and push it.  That way, it does
             * not matter whether the called function is compiled with
             * -mregparm or not.  */
            "movl %4,%%eax\n\t"
            "pushl %%eax\n\t"       
            "call *%5\n\t"      /* call fn */
            "movl %3,%0\n\t"    /* exit */
            "int $0x80\n"
            "1:\t"
            :"=&a" (retval), "=&S" (d0)
            :"0" (__NR_clone), "i" (__NR_exit),
             "r" (arg), "r" (fn),
             "b" (flags | CLONE_VM)
            : "memory");
        return retval;
    }
    

    So yes, as you suspect, the movl into eax is exactly to provide compatibility for both functions compiled with -mregparam or an equivalent __attribute__ and without (i386 System V ABI calling convention, i.e. all parameters on the stack).

    But this causes a problem: the previously pushed pointer by pushl %edx could remain in the stack if no one pops it off.

    Well, true, but who cares about popping it off if the only thing we are doing right after calling the kernel thread function is pushl %eax; call do_exit? Doing movl %eax, 0(%esp) would work as well, but if there's enough stack space to run a kernel thread then surely there are 4 bytes to push a register after it returns. Additionally pushl %eax is only 1 byte vs 3 for the movl, and I'm not sure about latency or throughput of the two, but I don't really think that was a concern in such a piece of code.