1

I have experimented with a userspace thread using C++. In the current version of the code, I am only able to point stack pointer to a specific function pointer but not sure how to place arguments.

Here is the basic implementation without function argument for the child thread. In the current implementation, I kept only two threads.

// File: main.cpp
/* Ubuntu 18.04 **x86_64** */
/* Compiler gcc version 7.5.0 */
/* Linux 5.6.2 */
/* g++ -g -std=c++11 main.cpp ContextSwitch.S */

#include <iostream>

const int __stack_size = 1 << 20;       // 1 MB


#define PrintF printf("%d : %s\n",__LINE__,__FUNCTION__);


// A Set of Registers to Represent the Context of Thread
// These register values are presered across function calls
// in x86-64 ABI
struct Context {
    uint64_t rsp;
    uint64_t r15;
    uint64_t r14;
    uint64_t r13;
    uint64_t r12;
    uint64_t rbp;
    uint64_t rbx;
};


// This is a function defined inside an Assembly file
// According to x86-64 ABI convention
// rdi == curr and rsi == next
extern "C" void ContextSwitch(Context *curr,Context *next);


// Lets take only two Thread
static struct Context Main;
static struct Context Child;


// This function helps to get back control from Child Thread
// When Child thread completes its assigned function
// this following function will be invoked
static void sched() {
    PrintF

    // now please switch back to Main thread
    ContextSwitch(&Child,&Main);
    std::cout <<"Not Reachable !" << std::endl;

}


// Child thread is going to run this function
void func() {
    PrintF

    // some dummy calculation inside this function

    int aa[100]; // 400 Bytes
    for(int i = 0;i<100;i++) {
        if(i%20 == 0) {
            aa[i] = 2*i;
        }
    }

    // on return from this function
    // ret instruction will be called
    // due to which rip will point to the value of 
    // top 8 byte of childStack
    // which is basically sched function
}

typedef unsigned long long ULL;

int main() {

    // allocate stack memory for the Child Thread
    char * childStack = new char[__stack_size];
    

    // sched() will be pointed by rip when func() returns
    *(ULL *)&childStack[__stack_size -  8] = (ULL)sched;


    // func() will be pointed by rip just before ContextSwitch() returns below
    *(ULL *)&childStack[__stack_size -  16] = (ULL)func;


    // when ContextSwitch tries to return from the call below,
    // rsp value will be loaded in rip due to ret call
    // which makes rip == addr of func()
    Child.rsp = (uint64_t)&childStack[__stack_size - 16];

    ContextSwitch(&Main,&Child);

    std::cout <<"Main is schduled again!" << std::endl;
    delete[] childStack;
}

// File: ContextSwitch.S
.globl ContextSwitch
ContextSwitch:

        movq     %rsp, 0x00(%rdi)
        movq     %r15, 0x08(%rdi)
        movq     %r14, 0x10(%rdi)
        movq     %r13, 0x18(%rdi)
        movq     %r12, 0x20(%rdi)
        movq     %rbp, 0x28(%rdi)
        movq     %rbx, 0x30(%rdi)

        movq     0x00(%rsi), %rsp
        movq     0x08(%rsi), %r15
        movq     0x10(%rsi), %r14
        movq     0x18(%rsi), %r13
        movq     0x20(%rsi), %r12
        movq     0x28(%rsi), %rbp
        movq     0x30(%rsi), %rbx

        ret

This implementation executes in my machine with the following output:

54 : func
43 : sched
Main is schduled again!

Now I tried with argument int argument in func(). (Although have not actually passed anything)

// Child thread is going to run this function
void func(int arg1) {
    PrintF

    // some dummy calculation inside this function

    int aa[100]; // 400 Bytes
    for(int i = 0;i<100;i++) {
        if(i%20 == 0) {
            aa[i] = 2*i;
        }
    }

    // on return from this function
    // ret instruction will be called
    // due to which rip will point to the value of 
    // top 8 byte of childStack
    // which is basically sched function
}

GDB disassembly shows that the argument int arg1 is expected in the lower 32 bits of %rdi.

Dump of assembler code for function func(int):
   0x0000000000000a69 <+0>: push   %rbp
   0x0000000000000a6a <+1>: mov    %rsp,%rbp
   0x0000000000000a6d <+4>: sub    $0x1c0,%rsp
   0x0000000000000a74 <+11>:    mov    %edi,-0x1b4(%rbp)

If I try with three arguments as in void func(int arg1,int arg2,int agr3) then

   0x0000000000000a74 <+11>:    mov    %edi,-0x1b4(%rbp)
   0x0000000000000a7a <+17>:    mov    %esi,-0x1b8(%rbp)
   0x0000000000000a80 <+23>:    mov    %edx,-0x1bc(%rbp)

I also found online that, in x86_64 arguments are passed on stack also (sometimes, I am not sure about this).

My question is how to pass arguments to func reliably ? Do I need to manually copy arguments into rdi,rsi and rdx as we did for context switch? What about more number of arguments and other types of arguments like reference type for example lvalue reference or rvalue reference in C++?

Thanks!

Debashish
  • 1,155
  • 19
  • 34
  • 3
    Consult ABI documentation for the particular details, they aren't pretty. The usual trick is to pass a single `void*` argument which the callee can then use to access a `struct` if needed to get more arguments. – Jester Jan 12 '21 at 20:54
  • and this `void*` can always be loaded in `%rdi` in `x86_64`?? – Debashish Jan 12 '21 at 20:55
  • 1
    That is correct. – Jester Jan 12 '21 at 20:57
  • 1
    Mostly void* should contain pointers to dynamically allocated variables. Or even better smart pointers. Since the thread from which you start the thread could quit the scope. It is better using std::shared_ptr for this kind of thing. – user123 Jan 12 '21 at 20:58
  • 2
    This is not something you want to do yourself. There is not a standard ABI so anytiing you do is locked to what you are working on. What you are looking for is co-routines. They are in boost and I also think they are in the latest version of the standard C++20 (though I would check that). – Martin York Jan 12 '21 at 21:37
  • 1
    Yes, I am actually aware of coroutine in C++20. I want to get the minimal possible basic implementation idea of coroutine before we start learning C++20's raw coroutine APIs. – Debashish Jan 12 '21 at 21:47
  • 2
    [names with double underscores are reserved](https://stackoverflow.com/q/228783/995714) so don't use `const int __stack_size` – phuclv Jan 13 '21 at 03:09

0 Answers0