layout: post title: "Process Syscalls" subtitle: "Almost caught up..." tags: [osdev] I've described my [syscall interface](/blog/2013/06/System-Calls/) previously. I've also described the [file-related syscalls](/blog/2013/12/VFS-syscalls/). In order to build [newlib](http://wiki.osdev.org/Porting_Newlib), some more syscalls are required. Those are: :::c void *sbrk(int incr); int getpid(); int fork(); void _exit(int rc); int wait(int *status); int kill(int pid, int sig); int execve(char *name, char **argv, char **env); Let's just go through them one at a time: ###sbrk `sbrk` is a bit special, since it actually has two versions - one for kernel use and one for user space processes. The user space one makes use of the [process memory manager](/blog/2013/06/Even-More-Memory/) to return a chunk of new memory for the `malloc` functions. :::c void *usr_sbrk(int incr) { process_t *p = current->proc; mem_area_t *area = find_including(p, p->mm.data_end); if(area) { if(area->end > (p->mm.data_end + incr)) { // The current memory area is large enough } else { // Increase memory area new_area(p, area->end, p->mm.data_end + incr, \ MM_FLAG_READ | MM_FLAG_WRITE | MM_FLAG_CANSHARE, \ MM_TYPE_DATA); } } else { // Create a new memory area new-area(p, p->mm.data_end, p->mm.data_end + incr, \ MM_FLAG_READ | MM_FLAG_WRITE | MM_FLAG_CANSHARE, \ MM_TYPE_DATA); } p->mm.data_end = p->mm.data_end + incr; return (void *)(p->mm.data_end - incr); } The kernel space version is just a simple linear allocator :::c uintptr_t kmem_top = KERNEL_HEAP_START; uintptr_t kmem_ptr = KERNEL_HEAP_START; void *sbrk(int incr) { if(kmem_ptr + incr > KERNEL_HEAP_END) { // PANIC! ... } while(kmem_top < kmem_ptr + incr) { vmm_page_set(kmem_top, vmm_page_val(pmm_alloc_page(), \ PAGE_PRESENT | PAGE_WRITE)); kmem_top += PAGE_SIZE; } kmem_ptr = kmem_ptr + incr; return (void *)kmem_ptr - incr; } Hopefully it's obvious why the kernel one is called `sbrk` while the user one has a different name. ###getpid `getpid` is rather obvious: :::c int getpid() { return current->proc->pid; } ###fork `fork` clones the current process and starts a new thread of execution. :::c int fork() { process_t *child = fork_process(); thread_t *ch_thread = list_entry(child->threads.next, thread_t, process_threads); ch_thread->r.eax = 0; scheduler_insert(ch_thread); return child->pid; } ###_exit `_exit` stops a program and wakes up any processes that are sleeping on it. :::c void _exit(int rc) { process_t *p = current->proc; // Close all open files int i; for(i = 0; i < NUM_FILEDES; i++) { if(p->fd[i]) close(i); } exit_process(current->proc, rc); current->state = THREAD_STATE_FINISHED; schedule(); } `_exit` doesn't return, and in fact `schedule()` will never return as far as this thread is concerned. Note that the process still exists. It is not completely destroyed until its parent process has executed a `wait` syscall. ###wait Actually, I didn't quite implement `wait` yet, but instead use a `waitpid` for now, which is a bit more specific: :::c int waitpid(int pid) { process_t *proc = get_process(pid); while(proc->state != PROC_STATE_FINISHED) { scheduler_sleep(current, &proc->waiting); schedule(); } int ret = proc->exit_code; free_process(proc); return ret; } This _should_ contain a check that process `pid` is a child of the current process too... ###kill I'll let `kill` wait for now. My next post will probably be on signals, so it'll fit better there anyway. ###execve Now, here's the big stuff. `execve` launches new programs from the filesystem, so what it has to do is: - Find the correct executable - Save the arguments - Save the environmental variables - Free the user memory space - Load the executable - Prepare a new user stack - Restore the arguments and environment variables First of all, the executable is found. If it doesn't exist, we want to fail as early as possible - before we destroy everything. :::c int execve(char *name, char **argv, char **env) { INODE executable = vfs_namei(name); if(!executable) { errno = ENOENT; return -1; } ... The arguments and environment are null-terminated lists of strings stored in user space, so they have to be copied into kernel space before the user space is destroyed: :::c ... usigned int envc = 0; char **temp_env = 0; if(env) { while(env[envc++]); // Count number of environmental variables temp_env = calloc(envc, sizeof(char *)); unsigned int i = 0; while(env[i]) { temp_env[i] = strdup(env[i]); i++; } } // Do the same thing for argv ... Next, Delete all memory from the previous executable and [load the new one](/blog/2013/08/Loading-Elf/): :::c procmm_removeall(current->proc); load_elf(executable); current->r.eax = current->r.ebx = current->r.ecx = \ current->r.edx = 0; We need to put the arguments and environment back into the new executable's user space, so a new stack area is created: :::c new_area(current->proc, USER_STACK_TOP, USER_STACK_TOP, \ MM_FLAG_WRITE | MM_FLAG_GROWSDOWN | MM_FLAG_ADDONUSE, \ MM_TYPE_STACK); current->kernel_thread = (registers_t *)current; uint32_t *pos = (uint32_t *)USER_STACK_TOP; Then, copy the environment and arguments onto the stack: :::c if(env) { pos = pos - envc*sizeof(char *)/sizeof(uint32_t) - 1; env = (char **)pos; int i = 0; while(temp_env[i]) { pos = pos - strlen(temp_env[i])/sizeof(uint32_t) - 2; memcpy(pos, temp_env[i], strlen(temp_env[i])+1); env[i] = (char *)pos; i++; } env[envc-1] = 0; } // Do the same for argc ... And finally, push the argument count, argument list and environment list onto the stack: :::c pos = pos - 3; pos[0] = (uint32_t)argc - 1; pos[1] = (uint32_t)argv; pos[2] = (uint32_t)env; current->r.useresp = current->r.ebp = (uint32_t)pos; current->r.ecx = (uint32_t)pos; return 0; } This pushes argc, argv and env as arguments to the executabl. We can use this to set up the `environ` variable of newlib. The crt0 in newlib pushes `ecx` to the stack and then calls `_init` which looks like this: :::c extern char **environ; void _init(uint32_t *args) { int argc; char **argv; if(args) { argc = args[0]; argv = (char **)args[1]; environ = (char **)args[2]; } else {...} exit(main(argc, argv)); }