I spent a lot of yesterday combing through man
pages. man fork
, man wait
, man sleep
... sounds like an axiom of some sort. I was trying to debug a simple program in assembly that did three things:
exec
from the child processwait
from the parent process to resume running until the child process completes.The first two steps went fine. I'd already performed these separately in different programs, so combining them was straightforward. We start out by immediately performing a fork
system call:
mov eax, 2 ; fork() syscall
int 0x80
cmp eax, 0 ; check if we are in the child process
jz child ; if eax == 0, jump to child
In the child process we request the execve
system call by moving the correct arguments:
child:
mov eax, 0x0b ; syscall number 11 for execve
mov ebx, filename
mov ecx, argv
mov edx, envp
int 0x80
In gdb
we can validate the process is forked and the child loads and executes the new program:
$ gdb
(gdb) layout asm
(gdb) break child
(gdb) set follow-fork-mode child
(gdb) run
We can stepi
until we get to the exec call and see the ./sleep
program from yesterday is loaded.
I had trouble with the wait
system call in the parent. After I made the wait call (which is code 7) it seemed like it never got successfully invoked. It turns out I wasn't paying enough attention to the required system call arguments. Let's have a closer look with man wait
.
wait
,waitpid
,waitid
- wait for process to change statepid_t waitpid(pid_t pid, int *wstatus, int options); Our system call requires three arguments that are provided in the
ebx,
ecx, and
edx` respectively:
pid_it d
: the pid
of the chid process to wait for. If this is set to 0, the parent process will wait for any child who's process group ID is equal to that of the calling process. If set to -1
the parent will wait for any child to terminate. We'll just use 0
for now.int *wstatus
: a pointer to an integer where the status information of the child is stored.int options
: controls the behavior of the system call. We can ignore this for now and just use 0
.section .bss
status resd 1 ; reserve space for the status variable
...
_start:
...
mov eax, 7 ; wait() syscall number
mov ebx,0
mov ecx,status ; pointer to status variable (for exit status)
mov edx,0
int 0x80
When I first wrote the program, I'd neglected to add the uninitialized variable status
in the .bss
section. The system call failed, but I didn't know why. When I inspected the registers after the call, however, I saw -22
in the eax
register. With man errno
we see the 22nd entry to be EINVAL
, or Invalid Argument
. Aside: why are these entries not numbered in the man pages? I had to actually count down 22 error codes to get to EINVAL
! :thinking_face:
These types of rejections are security mechanisms implemented into the kernel.
From OSTEP:
...the OS must check what the user passes in and ensure that arguments are properly specified, or otherwise reject the call ... In general, a secure system must treat user inputs with great suspicion. Not doing so will undoubtedly lead to easily hacked software, a despairing sense that the world is an unsafe and scary place, and the loss of job security for the all-too-trusting OS developer. [6]
Anyway, after fixing this issue and allocating a memory address in the .bss
section our program runs, forks, executes in the child, and waits in the parent for the child process to terminate. Success!
Here's the entire program:
section .data
filename db './sleep', 0 ; the filename (path to the program)
arg1 db './sleep', 0 ; argv[0] (the program name)
argv dd arg1, 0 ; argv array: {arg1, NULL}
envp dd 0 ; envp array: {NULL}
parent_waiting_message db "Parent process is waiting for child process to complete",0xa
parent_waiting_length equ $ - parent_waiting_message
child_completed_message db "Child process complete",0xa
child_completed_message_length equ $ - child_completed_message
child_message db "Child process created, loading new program",0xa
child_length equ $ - child_message
section .bss
status resd 1 ; reserve space for the status variable
section .text
global _start
_start:
mov eax, 2 ; fork() syscall
int 0x80
cmp eax, 0 ; check if we are in the child process
jz child ; if eax == 0, jump to child
parent:
mov ecx, parent_waiting_message
mov edx, parent_waiting_length
call print
mov eax, 7 ; wait() syscall number
mov ebx,0
mov ecx,status ; pointer to status variable (for exit status)
mov edx,0
int 0x80 ; invoke the system call
jnz exit_with_error ; check if wait() returned an error
mov ecx, child_completed_message
mov edx, child_completed_message_length
call print
call exit
child:
mov ecx, child_message
mov edx, child_length
call print
call exec
exec:
mov eax, 0x0b ; syscall number 11 for execve
mov ebx, filename ; filename -> ebx
mov ecx, argv ; argv -> ecx
mov edx, envp ; envp -> edx
int 0x80 ; make the system call
call exit
print:
mov ebx, 1 ; first argument: file handle (stdout)
mov eax, 4 ; system call number (SYS_WRITE)
int 0x80
ret
exit:
mov ebx, 0 ; return code
mov eax, 1 ; syscall number for exit
int 0x80
exit_with_error:
mov ebx, eax ; return error code in ebx
mov eax, 1 ; syscall number for exit
int 0x80
I've added some system call to print status strings to stdout
, as well as added a label to explicitly exit the program with a status code of 1 if the wait process fails.
I'm still interested in digging into the fork system call, specifically investigating how memory is locked and freed during context switching. Maybe performing some write system calls before exec is called in the child would illuminate some of the inner workings of these safety mechanisms, like "copy on write'".