To learn multithreading, I use code given by the teacher, that works for my classmates, but not for me.
The error happens here:
void fonction2(){
int compteur1 = 1;
while(1){
fflush(stdout);
printf("je suis la coroutine1\n");
fflush(stdout);
printf("blabla");
fflush(stdout);
printf("test\n");
printf("test2\n");
printf("compteur = %d\n", compteur1);
fflush(stdout);
compteur1 = compteur1 +1;
//yield();//appel la coroutine2
exit(1);
}
}
I'm not just looking for how to make my code work as intended, I'd like to understand what went wrong.
I compile using gcc 11.3.0 with flags -static -g -O0 in a Ubuntu1~22.04 VM.
Here is the entirety of my code:
#include<stdio.h>
#include<stdlib.h>
#define STACK_SIZE 4096
char pile1[STACK_SIZE] __attribute__((aligned(4096)));
char pile2[STACK_SIZE] __attribute__((aligned(4096)));
char pile3[STACK_SIZE] __attribute__((aligned(4096)));
char pile4[STACK_SIZE] __attribute__((aligned(4096)));
typedef void * coroutine_t;
void enter_coroutine(coroutine_t coroutine);
void switch_coroutine(coroutine_t *ct1, coroutine_t ct2);
coroutine_t coroutine_ordo;
coroutine_t coroutine4;
coroutine_t coroutine2;
coroutine_t coroutine3;
struct thread{
coroutine_t coroutine;
int statut; //O prêt 1 bloqué
};
struct thread thread2;
struct thread thread3;
struct thread thread4;
//struct liste_thread{
// thread* premier;
//};
struct thread* thread_courant ;
//comme un push pour préparer le enter_coroutine
coroutine_t init_coroutine(void *stack_begin, unsigned int stack_size, void (*initial_pc)(void)){
char *stack_end = ((char *)stack_begin) + stack_size;
void* *ptr = (void**) stack_end;
ptr--;
*ptr = initial_pc; //program counter
ptr--;
*ptr = 0; //rbp
ptr--;
*ptr = 0; // rbx
ptr--;
*ptr = 0; //r12
ptr--;
*ptr = 0; //r13
ptr--;
*ptr = 0; //r14
ptr--;
*ptr = 0; //r15
return ptr;
}
void yield(void){
switch_coroutine(&thread_courant->coroutine, coroutine_ordo);
}
void fonction_ordo(){
while(1){
thread_courant = &thread2;
switch_coroutine(&coroutine_ordo, thread2.coroutine);
thread_courant = &thread3;
switch_coroutine(&coroutine_ordo, thread3.coroutine);
thread_courant = &thread4;
switch_coroutine(&coroutine_ordo, thread4.coroutine);
}
}
void fonction2(){
int compteur1 = 1;
while(1){
fflush(stdout);
printf("je suis la coroutine1\n");
fflush(stdout);
printf("blabla\n");
fflush(stdout);
printf("test\n");
printf("test2\n");
printf("compteur = %d\n", compteur1);
fflush(stdout);
compteur1 = compteur1 +1;
//yield();//appel la coroutine2
exit(1);
}
}
void fonction3(){
int compteur2 = 1;
while(1){
printf("je suis la coroutine3\n");
printf("compteur = %d\n",compteur2);
++compteur2;
yield();//appel la coroutine1;
}
}
void fonction4(){
int compteur2 = 1;
while(1){
printf("je suis la coroutine4\n");
printf("compteur = %d\n",compteur2);
++compteur2;
yield();//appel la coroutine1;
}
}
int main(){
setbuf(stdout, NULL);
coroutine_ordo = init_coroutine(pile1, STACK_SIZE, &fonction_ordo); //ordonnanceur !
printf("coroutine1 init\n");
thread2.coroutine = init_coroutine(pile2, STACK_SIZE, &fonction2);
printf("coroutine2 init\n");
thread3.coroutine = init_coroutine(pile3, STACK_SIZE, &fonction3);
printf("coroutine3 init\n");
thread4.coroutine = init_coroutine(pile4, STACK_SIZE, &fonction4);
printf("coroutine4 init\n");
enter_coroutine(coroutine_ordo);
return 0;
}
This is a file in assembly I also give to the compiler, where stack pointers are manipulated directly to move from a thread to another:
.global enter_coroutine, switch_coroutine
/* Note: le premier argument est dans ecx, le deuxieme dans edx. */
switch_coroutine:
push %rbp
push %rbx
push %r12
push %r13
push %r14
push %r15
mov %rsp,(%rdi) /* Store stack pointer to the coroutine pointer.. */
mov %rsi,%rdi /* Continue to enter_coroutine, mais echange les arguments d'abord. */
enter_coroutine:
mov %rdi,%rsp /* Load the stack pointer from the coroutine pointer. */
pop %r15
pop %r14
pop %r13
pop %r12
pop %rbx
pop %rbp
ret /* Pop the program counter. */
I tried tinkering with the code to understand the issue; there is always a segmentation fault somewhere in function2, but this displays (other stuff from the rest of the code) je suis la coroutine1 Erreur de segmentation (core dumped)
However, if I add "\n" right after "blabla" then it displays (other stuff) je suis la coroutine1 blabla test test2 Erreur de segmentation (core dumped)
I thought using fflush would guarantee the same behavior regardless of what argument I give printf? Why does it not display the test prints in the first case?
The only thing needed to get the code to work is a small change to
init_coroutineto add a dummy 8-byte value to the base of the coroutine's stack:As long as
stack_endis on a 16-byte boundary (by suitable choice ofstart_beginandstack_sizeparameters), this will keep %RSP+8 aligned to a 16-byte boundary at the start of each function as required by the System V ABI for x86-64.The
initial_pcvalue is stored on a 16-byte boundary. Afterenter_coroutine()executes theretinstruction for the first time, thisinitial_pcvalue will be in %RIP, and %RSP+8 will be at a 16-byte boundary as required by the ABI. (The coroutine function will reduce %RSP by an odd multiple of 8 bytes if it calls another function, so that %RSP is maintained on a 16-byte boundary before it calls the function.1)―
1 Calling a function with arguments of type
__m256or__m512on the stack requires %RSP to be aligned to a 32- or 64-byte boundary before thecallinstruction.The reason why the original code goes wrong is that the GLIBC library functions assume that the stack pointer has been properly aligned when they are called. If that is not the case, bad things could happen.
The reason why adding a
\nto the end of a string literal (with no%characters) passed toprintfcan make a difference is that GCC may convertprintf("blabla\n");toputs("blabla");and a misaligned stack pointer inputsis probably not as bad as a misaligned stack pointer inprintf. (This conversion fromprintftoputscan be disabled with the-fno-builtin-printfGCC compiler option.)