Beginners Guide To x86 Shellcoding on FreeBSD

Картинки по запросу freebsd

( Original text by xwyzard )


The purpose of this tutorial is to help familiarize you with creating shellcode on the FreeBSD operating system. While I endeavor to explain everything in here thoroughly, this paper is not meant to be a primer on assembly coding. In the disassemblies you will notice that the assembly code is in AT&T syntax, while I much prefer to use Intel syntax (which is what nasm works on anyway). If you are concerned about the difference please do use google to find those differences. Please do note that I am just a beginner with shellcoding and that this is not meant, in any way, to be the end all, on the contrary this is meant to be an easy introduction for brand new shellcoders. In other words if you have written shellcodes, this is probably not going to interest you. The code within was adapted from linux code examples in The Shellcoders Handbook

Resources I used:

  • Unix Systems Programming
  • The Shellcoders Handbook,typeCd-NOTE.html
  • FreeBSD Assembly Language Programming by G. Adam Stanislav

Required tools:

  • objdump
  • NASM (Netwide Assembler)
  • GCC
  • gdb

Before we get started, lets save some time and grab a copy of /usr/src/sys/kern/syscalls.master This is the list of syscalls and their related numeric value. It is handy to keep a copy in your coding directory to save time on going to it, besides if you accidentally open that up and make changes while logged in as root, bad things could happen. Let’s play it safe and grab a copy.

Now that we’ve gotten that out of the way let’s dive right in and I’ll explain things as we move along. The first shellcode we will do is an extremely simple one, it is for exit(). We start by creating the exit() in C code, this will allow us to analyze the disassembly so that we can rewrite it into asm. Compile this up: gcc -o myexit myexit.c

/* As easy as it gets */  
exit(0); // exit with "0" for successful exit

Now that we have the compiled code we want to have a peek at the internals using gdb. This will allow us to see the computers «opinion» on what our code looks like in assembly. Just do the steps as I state them:

bash$ gdb myexit
(gdb) disas main
Dump of assembler code for function main:
: push %ebp
0x80481d9 : mov %esp,%ebp
0x80481db : sub $0x8,%esp
0x80481de : add $0xfffffff4,%esp
0x80481e1 : push $0x0
0x80481e3 : call 0x80498dc 
0x80481e8 : add $0x10,%esp
0x80481eb : nop
0x80481ec : leave
0x80481ed : ret
End of assembler dump.

Let’s break this down piece by piece. First, go ahead and don’t worry about anything up to and including Also, don’t be concerned about the addresses as mine are most likely going to be different than yours. Now look at , this is the first important part for our uses. That is the one and only parameter passed to exit(). Next is the actual call to exit. Those are the two main things we need from that. Before we get into the code, lets check syscalls.master for the value of sysexit() ‘grep’ping the file we find this line: 1 STD NOHIDE { void sysexit(int rval); } exit sysexitargs void The important information from that is 1 which is the syscall number value and the rval (return value) argument. This shows that sys_exit() takes one argument and we should know that a return value of ‘0’ is a successful exit.

Ok, on to putting it into assembly code.

section .text
global _start

xor eax, eax
push eax
push eax
mov eax, 1
int 80h

Take a look at the above code, now before we get into it much further a short explanation on why the code is done this way is in order. In FreeBSD (or NetBSD, OpenBSD) the parameters to a syscall are pushed onto the stack in reverse order, the actual syscall number placed into eax and then interrupt 80 to call the kernel to perform the work we setup.

Now to begin, we have ‘xor eax, eax’ what this does is zero’s out eax in the case there were any values into it alread. Then we ‘push eax’ twice. (I don’t know the technical reasons, but if zero is pushed onto the stack once, the exit call will return 1, we don’t want this, just push the zero value twice and save headaches.) Now we load up eax with the syscall value for exit which is 1. Last thing we must do is to actually call the kernel with ‘int 80h’

Great! Now we have something from which we can get shellcode! (Yes I know, finally!)

Alright well we need to assemble and then link this file.

bash$ nasm -f elf myexit.asm
bash$ ld -s -o myexit myexit.o

Now that it is assembled and linked we use objdump to get the shellcode from.

bash$ objdump -d myexit
shortexit: file format elf32-i386
/usr/libexec/elf/objdump: shortexit: no symbols
Disassembly of section .text:
08048080 <.text>:
8048080: 31 c0 xor %eax,%eax
8048082: 50 push %eax
8048083: 50 push %eax
8048084: b8 01 00 00 00 mov $0x1,%eax
8048089: cd 80 int $0x80

Looks beautiful doesn’t it? It might to someone, but it’s awful for us. Look at those NULLs in there (00), we can’t use that it will break as soon as we try to execute it in our C program. In C and in other languages, NULL will terminate a string. This means we are stuck like chuck if we try to load that into a C array. Well we can’t have that. There may be other ways to lean out this asm code, but I came up with this one.

Section .text
global _start

xor eax, eax
push eax
push eax
inc eax
int 80h

The only thing different here is the ‘inc eax’ this increments eax by 1 (remember eax started out at zero and we need 1 (exit syscall value) in it, so in this case it is identical to ‘mov eax, 1’.

Again, assemble and link this as shown on the last example and then use objdump.

bash$ objdump -d myexit

/usr/libexec/elf/objdump: exit_shellcode: no symbols
Disassembly of section .text:

08048080 <.text>:
8048080: 31 c0 xor %eax,%eax
8048082: 50 push %eax
8048083: 50 push %eax
8048084: 40 inc %eax
8048085: cd 80 int $0x80

Look at that! No NULLs in it, that’s a good one and we are going to keep it! Well now we have the proper shellcode with no NULLs in it, it is now time to load it up into a C program to execute it.

/*working shellcode */
char shellcode[] = "\x31\xc0\x50\x50\x40\xcd\x80";
int main()
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

That’s it, looks really pretty too! Now to compile that:

bash$ gcc -o shellcode shellcode.c
bash$ ./shellcode ; echo $?

Since we couldn’t really see much with an exit, we did ‘echo $?’. ‘$?’ is a bash builtin variable that holds the last exit code of a program. Since we gave exit ‘0’ return value we see our code worked! Good job, your patience and work has finally paid off. That was just the beginning though and you would not likely have a use for this code.

Well as you may have guessed, shellcode that exits isn’t very interesting or useful, however it is nice and easy to show the major points of creating shellcode. Now is where we get into one of the more common shellcodes and that is to utilize execve() to spawn a shell. But what else could we do with execve()? Tons, but that doesn’t matter right now. Before we go anywhere with this though, we should consult syscalls.master so we know exactly what execve() expects. Since execve is not at the very beginning of the file this is how I found it.

bash$ grep -i 'execve' syscalls.master
59 STD POSIX { int execve(char *fname, char **argv, char **envv); }

Now since we are going to be calling execve with no arguments, we only need to know what the first argument is. This is a pointer to the command we wish to execute. We will still need to keep the other arguments in mind since execve still expects to see them. So we put this call into C code so we have something with which to figure out the assembly code for it.

int main()
char *name[2];
name[0] = "/bin/sh";
name[1] = 0x0;
execve(name[0], name, 0x0);

Now compile that as we have shown and then fire up gdb:

bash$ gdb shell
(gdb) disas main
Dump of assembler code for function main:
: push %ebp
0x80484a1 : mov %esp,%ebp
0x80484a3 : sub $0x18,%esp
0x80484a6 : movl $0x8048503,0xfffffff8(%ebp)
0x80484ad : movl $0x0,0xfffffffc(%ebp)
0x80484b4 : add $0xfffffffc,%esp
0x80484b7 : push $0x0
0x80484b9 : lea 0xfffffff8(%ebp),%eax
0x80484bc : push %eax
0x80484bd : mov 0xfffffff8(%ebp),%eax
0x80484c0 : push %eax
0x80484c1 : call 0x8048350 
0x80484c6 : add $0x10,%esp
0x80484c9 : leave
0x80484ca : ret
0x80484cb : nop
End of assembler dump.

Wow that is alot to look at!

Since this one is so much longer, I will just skip to the code itself as the explanation should be clearer when you see the code. This is also why I am putting the explanation in the comments of this code.

;don't worry why this is here other than that it is required
;by ld. Just put it in there.
section .text
global _start
;We do this so that we can get the address of db '/bin/sh' onto the stack
jmp short _callshell
;This gets us the address of db '/bin/sh' into esi
pop esi
;ensure there are no values in eax
xor eax, eax
;now that eax is NULL, we will take a byte and put it to the end
;of the '/bin/sh' string to terminate it.
mov byte [esi + 7], al
;in freebsd assembly we put all the parameters onto the stack
;in reverse order. We are pushing eax twice which is null since we
;are not using execve() with parameters. However, this is still required
;by execve().
push eax
push eax
;last parameter for execve (note this is actually the first one required
;but this is reverse order.)
push esi
;Here's the actual syscall value for execve() we are moving it into
;al. If we were to put that value into eax we would get NULLs into
;our shellcode which is bad.
mov al, 0x3b 
;don't ask me why this is here, but it is required to have working shellcode
push eax 
;This is what will actually get the kernel involved and perform
;the work we have prepared for it above. note that this is interrupt 80h
int 0x80
;this takes us back up to the main portion of our code. The reason for
;this detour has been stated above for relative addresses.
call _shellcode
;our actual command string that will be fed into execve()
db '/bin/sh'

Now we assemble that file as so:

bash$ nasm -f elf mynewshell.asm
bash$ ld -o mynewshell mynewshell.o

Then we fire up objdump:

bash$ objdump -d mynewshell
mynewshell: file format elf32-i386
Disassembly of section .text:
08048080 <_start>:
8048080: eb 0e jmp 8048090 <_callshell>

08048082 <_shellcode>:
8048082: 5e pop %esi
8048083: 31 c0 xor %eax,%eax
8048085: 88 46 07 mov %al,0x7(%esi)
8048088: 50 push %eax
8048089: 50 push %eax
804808a: 56 push %esi
804808b: b0 3b mov $0x3b,%al
804808d: 50 push %eax
804808e: cd 80 int $0x80
08048090 <_callshell>:
8048090: e8 ed ff ff ff call 8048082 <_shellcode>
8048095: 2f das
8048096: 62 69 6e bound %ebp,0x6e(%ecx)
8048099: 2f das
804809a: 73 68 jae 8048104 <_callshell+0x74>

Have a look at all that beautiful shellcode. Now the tedious job of putting it into a usable format and right into a C program so that we can actually execute it.

/*working shellcode */
char shellcode[] = "\xeb\x0e\x5e\x31\xc0\x88\x46\x07\x50\x50\x56\xb0\x3b"
int main()
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;

Compile it:

bash$ gcc -o shell shell.c
bash$ ./shell

It worked! We have made shellcode that spawns a shell. That took awhile to get to and while this is certainly not the end to what you can do with shell code, it should give you the confidence to read the other, more thorough, tutorials out there and begin messing with shellcode on your own.

Special thanks to mardukk/push[eax] and int16h for their assistance on the more technical aspects that I was unsure of and to PrincesSoha for taking the time to edit and format my work far better than I could.

OpenBSD Kernel Internals — Creation of process from user-space to kernel space.

GDB + Qemu (env)

Hello readers,

I know this time it is a little late, but I am also busy with some other professional things. 🙂

This time let’s discuss about the process creation in OpenBSD operating system from user-space level to kernel space.

We will take an example of the user-space process that will be launched from the Command Line Interface (console), for example, “ls”, and then what happens in kernel-space as a result of it.

I will divide this series into 3 parts, like creationexecutionexit, because the creation of process itself took some amount of time for me to learn, and analyzing or tracking from user-space to kernel-space had to be done line by line.

I have used gdb to debug the process and analyze it line by line.

Now, I will not waste your time too much.

Let’s dive into the user-space to kernel-space and learn and see the beauty of puffer.

I have divided the full process and functions that are used in the kernel into the points, so, I think it will be easy to read and learn.

Now, suppose you have launched “ls” command from CLI (xterm):

Here, the parent process is “ksh”, that is, default shell in OpenBSD which invokes “ls” command or any other command.

Every process is created by sys_fork() , that is, fork system call which is indirectly (internally) calls fork1()

fork1 — kernel developer’s manual

fork1() creates a new process out of p1, which should be the current thread. This function is used primarily to implement the fork(2) and vfork(2) system calls, as well as the kthread_create(9) function.

Life cycle of a process (in brief):

“ls” → fork(2) → sys_fork() → fork1() → sys_execve() → sys_exit() → exit1()

Under the hood working of fork1()

After “ls” from user-space it goes to fork() (libc) then from there to sys_fork().


FORK_FORK: It is a macro which defines that the call is done by the fork(2)system call. Used only for statistics.

#define FORK_FORK 0x00000001

  • So, the value of flags variable is set to 1 , because the call is done by fork(2).
  • check for PTRACING then update the flags with PTRACE_FORK else leave it and return to the fork1()

Now, fork1()

fork1() initial code
  • The above code includes, curp->p_p->ps_comm is “ksh”, that is, parent process which will fork “ls” (user-space).
  • Initially some process structures, then, setting
    uid = curp->p_ucred->cr_ruid , it means setting the uid as real user id.
  • Then, the structure for process address space information.
  • Then, some variables and ptrace_state structure and then the condition checking using KASSERT.
  • fork_check_maxthread(uid) → it is used to the check or track the number of threads invoked by the specific uid .
  • It checks the number of threads invoked by specific uid shouldn’t be greater than the number of maximum threads allowed or also for maxthread —5 . Because the last 5 process from the maxthread is reserved for the root.
  • If it is greater than defined maxthread or maxthread — 5, it will print the messagetablefullonce every 10 seconds. Else, it will increment the number of threads.
  • Now, after fork_check_thread, again, the same implementation happens for tracking process. If you want you can have a look in our fork1 code screen-shot.

Now, we will proceed further,

fork1() code continued
  • It is changing the count of threads for a specific user via chgproccnt(uid,1).
  • uidinfo structure maintains every uid resource consumption counts, including the process count and socket buffer space usage.
  • uid_find function looks up and returns the uidinfo structure for uid. If no uidinfo structure exists for uid, a new structure will be allocated and initialized.

Then, it increments the ui_proccnt , that is, number of processes by diffand then returns count.

After, that, it is checks for the non-privileged uid and also that the number of process is greater than the soft limit of resources, that is, 9223372036854775807, from what I have found in gdb.

Have a look in the below screen-shot for the proper view of values:

(ddd) gdb output for resource limit

If non-privileged is allowed and the count is increased by the maximum resource limit, it will decrease the count via chgproccnt() by passing -1 as diff parameter and also decrease the number of processes and threads.

  • Next, the uvm_uarea_alloc() function allocates a thread’s ‘uarea’, the memory where its kernel stack and PCB are stored.

Now, it checks if the uaddr variable doesn’t contain any thread’s address, if it is zero, then it decrements the count of the number of process and thread.

Now, there are the some important functions:

→ thread_new(struct proc *parent, vaddr_t uaddr)

→ process_new(struct proc *p, struct process *parent, int flags)

thread_new(curp, uaddr)

Here, in the thread_new function, we will get our user-space process, that is, in our case “ls”. The process gets retrieved from the pool of process, that is, proc_pool via pool_get() function.

Then, we set the state of the thread to be SIDL , which means that the process/thread is being created by fork . We then setp →p_flag = 0.

Now, they are zeroing the section of proc . See, the below code snippet from sys/proc.h

code snippet for members that will be zeroed upon creation in fork, via memset

In above code snippet, all the variables will be zeroed via memset upon creation in the fork.

Then, they are copying the section from parent→p_startcopy to
p→p_startcopyvia memcpy. Have a look below in the screen-shot to know which of the field members will be copied.

code snippet for the members those will be copied upon in fork
  • The, crhold(p->p_ucred) means it will increment the reference count in struct ucred structure, that is, p->p_ucred->cr_ref++ .
  • Now, typecast the thread’s addr, that is, (struct user *)uaddr and save it in kernel’s virtual addr of u-area.
  • Now, it will initialize the timeout.

dummy function to show the timeout_set function working.

timeout_set(timeout, b, argument)

It means initialize the timeout struture and call the function b with argument .

timeout_set(struct timeout *new, void (*fn)(void *), void *arg)
        new->to_func = fn;
        new->to_arg = arg;
        new->to_flags = TIMEOUT_INITIALIZED;

scheduler_fork_hook(parent, p): It is a macro which will update the p_estcpu of child from parent’s p_estcpu.

p_estcpu holds an estimate of the amount of CPU that the process has used recently

/* Inherit the parent’s scheduler history */
#define scheduler_fork_hook(parent, child) do {    \
 (child)->p_estcpu = (parent)->p_estcpu;           \
} while (0)

Then, return the newly created thread p .

Now, another important function is process_new() which will create the process in a similar fashion to what we have seen above in the thread_newfunc.

  • process_new(struct proc *p, struct process *parent, int flags)

In above code snippet, the same thing is happening again like select process from process_pool via pool_get then zeroing using memset and copying using memcpy.

So, for the detailed explanation, please go through the thread_new() function first.

Next is initialization of process using process_initialize function.

process_initialize(pr, p)

ps_mainproc : It is the original and main thread in the process. It’s only special for the handling of p_xstat and some signal and ptrace behaviours that need to be fixed.

→Copy initial thread, that is, p to pr->mainproc .

→Initialize the queue with referenced by head. Here, head is pr→ps_threads. Then, Insert elm at the TAIL of the queue. Here, elm is p .

→set the number of references to 1, that is, pr->ps_refcnt = 1

→copy the process pr to the process of initial thread.

→set the same creds for process as the initial thread.

→condition check for the new thread and the new process via KASSERT.

→Initialize the List referenced by head. Here, head is pr->ps_children

→Again, initialize timeout. (for detail, see thead_new)

Now, after the process initialization, pid allocation takes place.

ps→ps_pid = allocpid(); allocpid() returns unused pid

allocpid() internally calls the arc4random_uniform() which again calls the arc4random() then via arc4random() a fully randomized number is returned which is used as pid.

Then, for the availability of pid, or in other words, for unused pid, it verifies that whether the new pid is already taken or not by any process. It verifies this one by one in the process, process groups, and zombie process by using function ispidtaken(pid_t pid) which internally calls these functions:

  • prfind(pid_t pid) : Locate a process by number
  • pgfind(pid_t pgid) : Locate a process group by number
  • zombiefind(pid_t pid :Locate a zombie process by number
code snippet for allocpid and ispidtaken

Now, store the pointer to parent process in pr→ps_pptr .

Increment the number of references count in process limit structure, that is, struct plimit .

Store the vnode of executable of parent into pr→ps_textvp ,that is, pr→ps_textvp = parent→ps_textvp; .

if (pr→ps_textvp)
        vref(pr→ps_textvp); /* vref --> vnode reference */

Above code snippet means, if valid vnode found then increment the v_usecount++ variable inside the struct vnode structure of the executable.

Now, the calculation for setting up process flags:

pr→ps_flags = parent →ps_flags & (PS_SUGID | PS_SUGIDEXEC | PS_PLEDGE | PS_EXECPLEDGE | PS_WXNEEDED);
pr →ps_flags = parent →ps_flags & (0x10 | 0x20 | 0x100000 | 0x400000 | 0x200000)
if (vnode of controlling terminal != NULL)
        pr→ps_flags |= parent→ps_flags & PS_CONTROLT;

process_new continued…

process_new continued…


* if child_able_to_share_file_descriptor_table_with_parent:
         pr->ps_fd = fdshare(parent)      /* share the table */
         pr->ps_fd = fdcopy(parent)       /* copy the table */
* if child_able_to_share_the_parent's_signal_actions:
         pr->ps_sigacts = sigactsshare(parent) /* share */
         pr->ps_sigacts = sigactsinit(parent)  /* copy */
* if child_able_to_share_the_parent's addr space:
         pr->ps_vmspace = uvmspace_share(parent)
         pr->ps_vmspace = uvmspace_fork(parent)
* if process_able_to_start_profiling:
         smartprofclock(pr);    /* start profiling on a process */
* if check_child_able_to_start_ptracing:
         pr->ps_flags |= parent->ps_flags & PS_PTRACED
* if check_no_signal_or_zombie_at_exit:
         pr->ps_flags |= PS_NOZOMBIE /*No signal or zombie at exit
* if check_signals_stat_swaping:
         pr->ps_flags |= PS_SYSTEM

update the pr→ps_flags with PS_EMBRYO by ORing it, that is,
pr→ps_flags |= PS_EMBRYO /* New process, not yet fledged */

membar_producer() → Force visibility of all of the above changes.

— All stores preceding the memory barrier will reach global visibility before any stores after the memory barrier reach global visibility.

In short, I think it is used to forcefully make visible changes globally.

Now, Insert the new elm, that is, pr at the head of the list. Here, head is allprocess .

  • return pr

fork1() continued…

fork1() continued…

p→p_fd and p→p_vmspace directly copy of pr→ps_fd and pr→ps_vmspace.



** if (process_has_no_signals_stats_or_swapping) then atomically set bits.

atomic_setbits_int(pr →ps_flags, PS_SYSTEM);

** if (child_is_suspending_the_parent_process_until_the_child_is terminated (by calling _exit(2) or abnormally), or makes a call to execve(2)) then atomically set bits,

atomic_setbits_int(pr →ps_flags, PS_PPWAIT);
atomic_setbits_int(pr →ps_flags, PS_ISPWAIT);

#ifdef KTRACE
/* Some KTRACE related things */

cpu_fork(curp, p, NULL, NULL, func, arg ?arg: p)

— To create or Update PCB and make child ready to RUN.

 * Finish creating the child thread. cpu_fork() will copy
 * and update the pcb and make the child ready to run. The
 * child will exit directly to user mode via child_return()
 * on its first time slice and will not return here.

Address space,
vm = pr→ps_vmspace

if (call is done by fork syscall); then
increment the number of fork() system calls.
update the vm_pages affected by fork() syscall with addition of data page and stack page.
else if (call is done by vfork() syscall); then
do as same as if it was fork syscall but for vfork system call. (see above if {for fork})
increment the number of kernel threads created.


If (process is being traced && created by fork system call);then
        The malloc() function allocates the uninitialized memory in the kernel address space for an object whose size is specified by size, that is, here, sizeof(*newptstat). And, struct ptrace_state *newptstat

allocate thread ID, that is, p→p_tid = alloctid();
This is also the same calling arc4random directly and using tfind function for finding the thread ID by number.

* inserts the new element p at the head	of the allprocess list.
* insert the new element p at the head of the thread hash list.
* insert the new element pr at the head of the process hash list.
* insert the new element pr after the curpr element.
* insert the new element pr at the head of the children process  list.

fork1() continued…

fork1 continued…

If (isProcessPTRACED())
then save the parent process id during ptracing, that is,
pr→ps_oppid = curpr→ps_pid .
If (pointer to parent process_of_child != pointer to parent process_of_current_process)
proc_reparent(pr, curpr→ps_pptr); /* Make current process the new parent of process child, that is, pr*/

Now, check whether newptstat contains some address, in our case, newptstat contains a kernel virtual address returned by malloc(9.
If above condition is True, that is, newptstat != NULL . Then, set the ptrace status:
Set newptstat point to the ptrace state structure. Then, make the newptstatpoint to NULL .

→Update the ptrace status to the curpr process and also the pr process.

curpr->ps_ptstat->pe_report_event = PTRACE_FORK;
pr->ps_ptstat->pe_report_event = PTRACE_FORK;
curpr->ps_ptstat->pe_other_pid = pr->ps_pid;
pr->ps_ptstat->pe_other_pid = curpr->ps_pid;

Now, for the new process set accounting bits and mark it as complete.

  • get the nano time to start the process.
  • Set accounting flags to AFORK which means forked but not execed.
  • atomically clear the bits.
  • Then, check for the new child is in the IDLE state or not, if yes then make it runnable and add it to the run queue by fork_thread_start function.
  • If it is not in the IDLE state then put arg to the current CPU, running on.

Freeing the memory or kernel virtual address that is allocated by malloc for newptstat via free .

Notify any interested parties about the new process via KNOTE .

Now, update the stats counter for successfully forked.

uvmexp.forks++; /* -->For forks */
if (flags & FORK_PPWAIT)
        uvmexp.forks_ppwait++; /* --> counter for forks where parent waits */
if (flags & FORK_SHAREVM)
        uvmexp.forks_sharevm++; /* --> counter for forks where vmspace is shared */

Now, pass pointer to the new process to the caller.

if (rnewprocp != NULL)
        *rnewprocp = p;
fork1 continued…
  • setting the PPWAIT on child and the PS_ISPWAIT on ourselves, that is, the parent and then go to the sleep on our process via tsleep .
  • Check, If the child is started with tracing enables && the current process is being traced then alert the parent by using SIGTRAP signal.
  • Now, return the child pid to the parent process.
  • return (0)

Then, finally, I have seen in the debugger that after the fork1, it jumps to sys/arch/amd64/amd64/trap.c file for system call handling and for the setting frame.

Some of the machine independent (MI) functions defined in sys/sys/syscall_mi.h file, like, mi_syscall()mi_syscall_return() and mi_child_return().

Then, after handling the system calls from trap.c then, control pass to the sys_execve system call, which I will explain later (in the second part) and also I will explain more about the trap.c code in upcoming posts. It has already become a long post.