Introduction to Processes#

A process is an executing program. It has a memory address space, a program image, handles to files, and the operating system state needed for interprocess communication.

Every process has a parent. In traditional UNIX systems, the top of the process tree is init. On Windows, several system processes fill similar roles, including system.exe for the kernel, csrss.exe for user-mode Win32 support, wininit.exe for system services, explorer.exe for the shell, and winlogin.exe for login services.

Process Memory Layout#

A process address space is normally divided into a few major regions. The text segment contains the machine instructions of the program. The data segment contains initialized static data and constants. The BSS segment contains uninitialized static data.

The heap is used for dynamic memory allocation. A process may have one or more heaps. The stack contains local variables, call frames, and return values. Kernel-mode threads normally have one stack per thread. User-mode threading libraries may multiplex many user threads over a smaller number of kernel-thread stacks.

The loader is responsible for laying out the text, data, BSS, initial heap, and initial stack. After the process starts, stack and heap management are handled by the program, its runtime library, and the operating system.

Process Memory Layout#

Memory Layout

Memory Layout#

Multithreaded Memory Layout#

Memory Layout for Multithreaded Programs

Memory Layout for Multithreaded Programs#

Preliminaries#

The code examples are maintained at gkthiruvathukal/systems-code-examples. You can use git clone https://github.com/gkthiruvathukal/systems-code-examples to clone them into a folder named systems-code-examples.

Full working demonstrations are referenced by directory name, such as systems-code-examples/fork. To build most examples, install gcc, cmake, and make. The examples have been tested on Ubuntu Linux, macOS, and Windows Subsystem for Linux 2 with Ubuntu.

Most examples use the same pattern:

$ cd systems-code-examples/<example-name>
$ cmake .
$ make

The output executable is normally written to the example’s bin subdirectory.

Examine Process Layout Example#

The systems-code-examples/c_intro example is a small C++ program used to examine process layout and the basic build process. It is also a useful first example because it has separate source files and a CMake build.

Get and build the code:

$ git clone https://github.com/SoftwareSystemsLaboratory/systems-code-examples
$ cd systems-code-examples/c_intro
$ cmake .
$ make

Run the layout shell script to show the size of the text, data, and BSS sections in bytes:

$ ./layout

section               size   addr
.text                 3957    4352
.data                   24   20480
.bss                     8   20504

The main program for this example is:

 1#include "list.hh"
 2#include "debug.hh"
 3#include "tests.hh"
 4#include <stdio.h>
 5
 6int main(int argc, char *argv[])
 7{
 8    int passCount = runTests();
 9
10    printf("%d tests passed\n", passCount);
11
12    return 0;
13}

Key points:

  • The program is split across multiple source files, so the build has to compile each file and link the resulting object files.

  • The example gives us an executable we can inspect with tools such as size and nm.

  • The layout script connects the compiled program back to the text, data, and BSS sections discussed above.

Loading Programs#

When a program is loaded, the loader allocates memory for the executable text, data, BSS, heap, and stack. It then loads the program image into memory.

The loader also handles shared libraries. It asks the operating system which shared libraries are already loaded and loads the ones that are not. Each shared library has its own text, data, and BSS sections. The loader then resolves external symbols so references in the executable point to the correct locations in memory.

The nm command shows symbols in a compiled object file or executable. After the executable is ready, the loader invokes _start(). _start() calls _init() for each shared library, initializes static constructors for global objects, and then calls main().

Simple Executable Example#

The systems-code-examples/hello-exe example is the smallest complete executable example in the repository. It is useful for confirming the build process before looking at larger programs.

1#include "hello.h"
2
3int main(int argc, char** argv)
4{
5    say_hi("George");
6}
7

Key points:

  • main() is the entry point that application programmers normally write, even though the loader starts execution before main().

  • The program calls a function from a header-defined interface rather than doing all work inline.

  • This is a minimal executable for checking compiler, linker, and loader behavior.

Loading shared libraries (.so)#

Libraries also have text, data, and BSS sections. Shared libraries must use position-independent memory references so the same library can be mapped at different addresses in different processes. GCC uses the -fpic or -fPIC flags for this. Newer GCC versions often make PIC the default. Use -fno-pic when you need to compare against non-PIC output.

The dynamic linker resolves position-independent memory accesses by writing entries in the global offset table, or GOT, for each process. This is necessary because a shared library may be loaded at different offsets in different executions, while still allowing the operating system to share the library’s text segment among processes.

Shared Library Example#

The systems-code-examples/hello-lib example separates a function into a small library and calls it from an executable. This is the simplest repository example for discussing the boundary between application code and library code.

1#include <stdio.h>
2#include "hello.h"
3
4void say_hi(char* person)
5{
6    printf("Hello, %s\n", person);
7}
8
1#ifndef _HELLO_H_
2
3#define _HELLO_H_
4
5extern void say_hi(char* person);
6
7#endif
8

Key points:

  • hello.h declares the interface that other source files can include.

  • hello.c provides the implementation that can be compiled into a library.

  • The executable depends on the library code being linked or loaded before the call to say_hi() can run.

more about GCC pic option#

Based on the man page for gcc:

Generate position-independent code (PIC) suitable for use in a shared library, if supported for the target machine. Such code accesses all constant addresses through a global offset table (GOT). The dynamic loader resolves the GOT entries when the program starts. If the GOT size for the linked executable exceeds a machine-specific maximum size, the linker reports that -fpic does not work. In that case, recompile with -fPIC instead.

Loading static libraries#

Static libraries do not contain position-independent code. A static library is a collection of unlinked .o object files. When the program is linked, the linker copies the needed object code into the final executable.

Position Independent Code Example#

The systems-code-examples/pic example shows the difference between generating PIC and non-PIC assembly.

Get the code and build it with the example makefile:

$ git clone https://github.com/SoftwareSystemsLaboratory/systems-code-examples
$ cd systems-code-examples/pic
$ make -f Makefile.pic

main.c#

 1#include <unistd.h>
 2
 3const char *msg = "abcdefg";
 4
 5int main(int argc, char* argv[])
 6{
 7
 8    const char *x = msg + 1;
 9
10    write(1, x, 1);
11
12    return (0);
13
14}
15

main.nopic.s - non-position independent code (gcc -fno-pic)#

 1	.file	"main.c"
 2	.text
 3	.globl	msg
 4	.section	.rodata
 5.LC0:
 6	.string	"abcdefg"
 7	.data
 8	.align 8
 9	.type	msg, @object
10	.size	msg, 8
11msg:
12	.quad	.LC0
13	.text
14	.globl	main
15	.type	main, @function
16main:
17.LFB0:
18	.cfi_startproc
19	endbr64
20	pushq	%rbp
21	.cfi_def_cfa_offset 16
22	.cfi_offset 6, -16
23	movq	%rsp, %rbp
24	.cfi_def_cfa_register 6
25	subq	$32, %rsp
26	movl	%edi, -20(%rbp)
27	movq	%rsi, -32(%rbp)
28	movq	msg(%rip), %rax
29	addq	$1, %rax
30	movq	%rax, -8(%rbp)
31	movq	-8(%rbp), %rax
32	movl	$1, %edx
33	movq	%rax, %rsi
34	movl	$1, %edi
35	call	write
36	movl	$0, %eax
37	leave
38	.cfi_def_cfa 7, 8
39	ret

main.nopic.s - position independent code (gcc -fpic, default option)#

 1	.file	"main.c"
 2	.text
 3	.globl	msg
 4	.section	.rodata
 5.LC0:
 6	.string	"abcdefg"
 7	.section	.data.rel.local,"aw"
 8	.align 8
 9	.type	msg, @object
10	.size	msg, 8
11msg:
12	.quad	.LC0
13	.text
14	.globl	main
15	.type	main, @function
16main:
17.LFB0:
18	.cfi_startproc
19	endbr64
20	pushq	%rbp
21	.cfi_def_cfa_offset 16
22	.cfi_offset 6, -16
23	movq	%rsp, %rbp
24	.cfi_def_cfa_register 6
25	subq	$32, %rsp
26	movl	%edi, -20(%rbp)
27	movq	%rsi, -32(%rbp)
28	movq	msg@GOTPCREL(%rip), %rax
29	movq	(%rax), %rax
30	addq	$1, %rax
31	movq	%rax, -8(%rbp)
32	movq	-8(%rbp), %rax
33	movl	$1, %edx
34	movq	%rax, %rsi
35	movl	$1, %edi
36	call	write@PLT
37	movl	$0, %eax
38	leave
39	.cfi_def_cfa 7, 8
40	ret

What’s the difference?#

 1diff main.pic.s main.nopic.s
 27c7
 3< 	.section	.data.rel.local,"aw"
 4---
 5> 	.data
 628,29c28
 7< 	movq	msg@GOTPCREL(%rip), %rax
 8< 	movq	(%rax), %rax
 9---
10> 	movq	msg(%rip), %rax
1136c35
12< 	call	write@PLT
13---
14> 	call	write

Key points:

  • The C source is intentionally small so the assembly differences are easier to see.

  • The non-PIC version can use more direct addressing because it assumes a fixed relationship between code and data.

  • The PIC version uses relative addressing through the global offset table so the code can be loaded at different virtual addresses.

Shared Libraries - Evaluation#

Shared libraries reduce memory use when multiple processes load the same library because the text segment can be reused. They also allow programs to benefit from library updates without rebuilding the executable.

The cost is complexity. Shared libraries require virtual memory support, dynamic linking, position-independent code, and careful handling of version and path compatibility.

Static Libraries - Evaluation#

Static libraries make sense when reuse is not important, when deployment must avoid runtime dependencies, or when the program must be self contained. Static linking can also avoid version and path problems.

The main cost is memory use. Common code is copied into each executable, so there is less sharing across processes.

Libraries vs. Statically-Linked Programs#

Dynamic linking is usually chosen for memory footprint, code reuse, smaller executables, and easier library updates. Static linking is useful when deployment simplicity matters more than sharing. It can also make whole-program optimization and obfuscation easier because more code is available to the linker at once.

Process Protection#

Modern operating systems use virtual memory and privilege separation to protect processes from each other and from the kernel.

One process cannot read another process’s memory unless the operating system explicitly permits it. A process can manage the memory it owns, including allocation, deallocation, garbage collection, calling conventions, and stack management. A crash, exception, resource starvation, or deadlock in one process does not directly modify another process. Even though kernel memory may be mapped into the process address space, user code cannot modify kernel memory or other protected pages.

Process Creation with fork()#

fork() creates a new process by duplicating the calling process. The new process is the child. The calling process is the parent. The child has its own process ID, and its parent process ID is the parent’s process ID. The parent’s threads are not recreated in the child.

In Linux, the C library implementation of fork() uses clone(). From the programmer’s point of view, fork() returns the child PID to the parent, returns 0 to the child, and returns -1 if the child cannot be created.

fork() example#

The systems-code-examples/fork example shows the basic return-value pattern used to distinguish parent and child execution.

$ cd systems-code-examples/fork
$ cmake .
$ make
 1#include <stdio.h>
 2#include <unistd.h>
 3#include <fcntl.h>
 4#include <string.h>
 5
 6int main(int argc, char* argv[])
 7{
 8    int SomeValue = 100;
 9    int pid = fork();
10    int fd = open("test_file", O_WRONLY|O_CREAT|O_TRUNC, 0666);
11
12    const char *parentMessage = "1111111";
13    const char *childMessage =  "22222222222222\n";
14
15    if(pid > 0)
16    {
17        printf("hello from the parent process, chid pid = %d\n", pid);
18        sleep(2);
19        printf("parent's SomeValue = %d\n", SomeValue);
20        write(fd, parentMessage, strlen(parentMessage) * sizeof(char));
21    }
22    else if(pid == 0)
23    {
24        printf("hello from the child process\n");
25        SomeValue = 200;
26        printf("child's SomeValue = %d\n", SomeValue);
27        write(fd, childMessage, strlen(childMessage) * sizeof(char));
28    }
29    else
30    {
31        printf("fork() failed!!\n");
32    }
33
34    close(fd);
35
36    return 0;
37}
38
39

Key points:

  • The call to fork() creates two processes that continue from the same point in the program.

  • The parent sees the child’s process ID as the return value.

  • The child sees 0 as the return value, which is how the code chooses the child branch.

fork() and exec() example#

The systems-code-examples/fork_exec example creates a pipe, forks, redirects standard input and standard output with dup2(), and then uses execvp() to replace each process image. The parent runs wc. The child runs echo.

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <fcntl.h>
 4#include <stdlib.h>
 5
 6int main(int argc, char* argv[])
 7{
 8
 9    int pipes[2];
10    pipe(pipes);
11
12    int inputPartOfPipe = pipes[0];
13    int outputPartOfPipe = pipes[1];
14
15    int pid = fork();
16
17    if(pid > 0)  	//parent process
18    {
19        dup2(inputPartOfPipe, 0); // redirect STDIN
20        close(outputPartOfPipe);  // close unused half of pipe
21
22        char *wcArgs[] = {"-l", NULL};
23        execvp("wc", wcArgs);
24
25        fprintf(stderr, "should not be able to reach here!\n");
26    }
27    else if(pid == 0)  	//child process
28    {
29        dup2(outputPartOfPipe, 1); // redirect STDOUT
30        close(inputPartOfPipe);     // close unused half of pipe
31
32        char *echoArgs[] = {"-ne", "\"hello\\nworld\\n\"\n", NULL};
33        execvp("echo", echoArgs);
34
35        fprintf(stderr, "should not be able to reach here!\n");
36    }
37    else
38    {
39        printf("fork failed!\n");
40    }
41
42    //don't worry about closing remaining pipes,
43    //process exit does this for us
44
45    return 0;
46}
47
48

Key points:

  • pipe() creates a kernel buffer with one file descriptor for reading and one for writing.

  • dup2() redirects standard input or standard output before the process image is replaced.

  • execvp() replaces the current program with another executable; code after a successful execvp() call does not run.

Process Creation with clone()#

clone() is similar to fork() because it creates a child execution context. Unlike fork(), it allows selected parts of the parent process to be shared with the child.

Linux uses flags to control what is shared. CLONE_FS shares file system information such as chroot, chdir, and umask. CLONE_FILES shares the file descriptor table. CLONE_SIGHAND shares signal handlers. CLONE_VM shares the page table. These flags are part of how Linux creates lightweight processes and kernel threads.

glibc’s fork() calls clone() without these sharing flags. clone() is Linux-specific and is not present in every UNIX-like operating system.

Windows CreateProcess() and CreateThread()#

Windows does not expose the same process-sharing model as UNIX fork() and Linux clone(). CreateProcess() creates a new process and is closest to calling fork() followed by execve() on UNIX. CreateThread() creates a new thread and is closest to using clone() with thread-sharing flags.

For most programs, this is not a practical disadvantage. Most UNIX uses of fork() are followed by exec(), and most uses of clone() are for thread creation.

Emulating fork() on Windows#

Cygwin implements fork() on Windows by combining CreateProcess(), saved register state, copied memory regions, and synchronization. The parent creates a suspended child process, saves register state with setjmp(), copies the BSS and data sections to the child, wakes the child, and then coordinates stack, heap, and memory-mapped region copies.

This approach is closer to “copy on fork” than modern UNIX copy-on-write. It is useful for compatibility, but it is more expensive than a native UNIX fork() implementation.

System Call Boundary Example#

The systems-code-examples/syscall example compares three ways to write output: the C library printf(), the POSIX write() wrapper, and a direct syscall() invocation.

 1#include <stdio.h>
 2#include <string.h>
 3#include <unistd.h>
 4#include <sys/syscall.h>
 5
 6int main(int argc, char* argv[])
 7{
 8    char* hello_with_syscall = "Hello World with syscall\n";
 9    char* hello_without_syscall = "Hello World without syscall\n";
10    char* hello_with_printf = "Hello World with printf\n";
11
12    write( 1, hello_without_syscall, strlen( hello_without_syscall ));
13
14    syscall( SYS_write, 1, hello_with_syscall, strlen( hello_with_syscall ));
15
16    printf( "%s", hello_with_printf );
17
18    return 0;
19}
20
21

Key points:

  • printf() is a C library call, not a direct system call.

  • write() is the usual POSIX wrapper for the kernel’s write system call.

  • syscall(SYS_write, ...) bypasses the wrapper and invokes the system call interface directly.

Command-Line Option Example#

The systems-code-examples/getopt example shows how a process receives command-line arguments through argc and argv and turns them into structured options.

 1#include "cl.h"
 2
 3int main(int argc, char *argv[])
 4{
 5
 6    my_options_t* options = my_program_options_get(argc, argv);
 7    if (options->success)
 8    {
 9        print_options(options);
10        exit(EXIT_SUCCESS);
11    }
12    else
13    {
14        exit(EXIT_FAILURE);
15    }
16}
 1#ifndef _CL_H_
 2#define _CL_H_
 3
 4#include <ctype.h>
 5#include <stdio.h>
 6#include <stdlib.h>
 7#include <unistd.h>
 8#include <regex.h>
 9
10typedef struct my_options_t
11{
12    int how_many;
13    int last_n_words;
14    int min_length;
15    int every_steps;
16    int ignore_case;
17    int success;
18    int print_options;
19    int do_timing;
20    regex_t regex;
21
22} my_options_t;
23
24extern my_options_t *my_program_options_new();
25extern void my_mrogram_options_delete(my_options_t *options);
26extern my_options_t* my_program_options_get (int argc, char **argv);
27extern void print_options(my_options_t *options);
28
29#endif

Key points:

  • argc and argv are the process’s command-line interface.

  • The parsing code converts raw strings into a structured options object.

  • The program exits with success or failure depending on whether the command line was valid.

Process Termination#

A process can terminate normally by returning from main(). It can also terminate with an error code, by encountering a fatal error, or by being terminated externally by another process.

Fatal errors include segmentation faults or bus errors from invalid memory access, stack overflow, protection faults from privileged instructions, and instruction faults such as divide by zero.

wait() and waitpid() examples#

The systems-code-examples/wait example shows how a parent process can wait for a child process to terminate.

 1#include <sys/types.h>
 2#include <sys/wait.h>
 3#include <stdio.h>
 4#include <stdlib.h>
 5#include <unistd.h>
 6
 7
 8int main(int argc, char* argv[])
 9{
10    pid_t pid = fork();
11    if(pid == 0)
12    {
13        abort();    //child process exits
14    }
15    int status;
16    wait(&status); // wait for child to exit
17    if(WIFEXITED(status))
18    {
19        printf("normal exit. exit code = %d\n", WEXITSTATUS(status));
20    }
21    else if(WIFSIGNALED(status))
22    {
23        printf("abnormal termination, signal number = %d\n", WTERMSIG(status));
24    }
25    else if(WIFSTOPPED(status))
26    {
27        printf("child stopped, signal number = %d\n", WSTOPSIG(status));
28    }
29}
30
31

Key points:

  • The parent process uses wait() or waitpid() to collect child exit status.

  • Waiting also prevents terminated children from remaining as zombie processes.

  • The status value encodes how the child terminated.

SIGCHLD examples#

The systems-code-examples/fork_sigchld example handles SIGCHLD to notice when a child process exits. It uses wait() in the signal handler to collect the child’s exit status.

 1#include <stdio.h>
 2#include <string.h>
 3#include <signal.h>
 4#include <unistd.h>
 5#include <sys/wait.h>
 6#include <sys/types.h>
 7
 8int child_exit_status = (-5);
 9
10void clean_up_child_process(int signal_number)
11{
12    printf("SIGCHLD recieved.\n");
13    int status;
14    wait(&status);
15    child_exit_status = status;
16}
17
18void parent()
19{
20    printf("parent continuing....");
21    while(child_exit_status == (-5))
22    {
23        printf("parent waiting...\n");
24        sleep(1);
25    }
26    printf("child exit status = %d\n", child_exit_status);
27    printf("parent exiting\n");
28}
29
30void child()
31{
32    printf("child starting.\n");
33    sleep(5);
34    printf("child exiting.\n");
35}
36
37int main(int argc, char** argv)
38{
39
40    struct sigaction sigchld_action;
41    memset (&sigchld_action, 0, sizeof (struct sigaction));
42    sigchld_action.sa_handler = &clean_up_child_process;
43    sigaction (SIGCHLD, &sigchld_action, NULL);
44
45    if(fork() > 0)
46    {
47        parent();
48    }
49    else
50    {
51        child();
52    }
53
54    return 0;
55}
56
57

Key points:

  • sigaction() installs a handler for SIGCHLD before the fork.

  • The kernel sends SIGCHLD to the parent when the child exits.

  • The handler calls wait() to collect the child’s exit status.

The systems-code-examples/fork_sigchld2 example extends this pattern to multiple child processes. It uses waitpid() with WNOHANG so the handler can collect every child that has exited without blocking.

  1#include <stdio.h>
  2#include <string.h>
  3#include <signal.h>
  4#include <unistd.h>
  5#include <sys/wait.h>
  6#include <sys/types.h>
  7
  8#define MAX_PID (1000)
  9
 10int childProcesses[MAX_PID];
 11
 12void addProcess(int childPid)
 13{
 14    printf("add(%d)\n", childPid);
 15    for(int i = 0; i < MAX_PID; i++)
 16    {
 17        if(childProcesses[i] == (-1))
 18        {
 19            childProcesses[i] = childPid;
 20            printf("added child pid = %d\n", childPid);
 21            return;
 22        }
 23    }
 24}
 25
 26void removeProcess(int childPid)
 27{
 28    printf("remove(%d)\n", childPid);
 29    for(int i = 0; i < MAX_PID; i++)
 30    {
 31        if(childProcesses[i] == childPid)
 32        {
 33            childProcesses[i] = (-1);
 34            printf("removed child pid = %d\n", childPid);
 35        }
 36    }
 37}
 38
 39int countProcesses()
 40{
 41    int count = 0;
 42    for(int i = 0; i < MAX_PID; i++)
 43    {
 44        if(childProcesses[i] >= 0)
 45        {
 46            count += 1;
 47        }
 48    }
 49    return count;
 50}
 51
 52void clean_up_child_process(int signal_number)
 53{
 54    printf("SIGCHLD recieved.\n");
 55    int status;
 56    pid_t pid;
 57    while((pid = waitpid(-1, &status, WNOHANG)) > 0)
 58    {
 59        if(pid > 0)
 60        {
 61            printf("pid %d exited\n", pid);
 62            removeProcess((int)pid);
 63        }
 64    }
 65    return;
 66}
 67
 68void parent()
 69{
 70    printf("parent continuing....\n");
 71    while(countProcesses() > 0) { }
 72    printf("parent exiting\n");
 73}
 74
 75void child()
 76{
 77    printf("child starting.\n");
 78    sleep(5);
 79    printf("child exiting.\n");
 80}
 81
 82int main(int argc, char** argv)
 83{
 84    for(int i = 0; i < MAX_PID; i++)
 85    {
 86        childProcesses[i] = (-1);
 87    }
 88
 89    struct sigaction sigchld_action;
 90    memset (&sigchld_action, 0, sizeof (struct sigaction));
 91    sigchld_action.sa_handler = &clean_up_child_process;
 92    sigaction (SIGCHLD, &sigchld_action, NULL);
 93
 94    for(int i = 0; i < 5; i++)
 95    {
 96        int childProcess = fork();
 97        if(childProcess == 0)
 98        {
 99            child();
100            return 0;
101        }
102        else if(childProcess > 0)
103        {
104            addProcess(childProcess);
105        }
106        else
107        {
108            printf("fork failed\n");
109        }
110    }
111    parent();
112
113    return 0;
114}
115
116

Key points:

  • The parent tracks child process IDs in a small table.

  • The signal handler loops with waitpid(-1, ..., WNOHANG) to collect all children that have exited.

  • WNOHANG matters because a signal handler should not block waiting for children that are still running.

Process-Oriented C Case Studies#

Some examples in the repository are best treated as short systems C case studies. They support the process chapter because they make memory, calling conventions, and low-level representation concrete.

The systems-code-examples/objects example shows object-style programming in C using struct values and functions.

 1#include <stdio.h>
 2#include <string.h>
 3#include <stdlib.h>
 4
 5typedef struct
 6{
 7    char Name[30];
 8    int Age;
 9    int NumberOfTeamMates;
10    struct Employee** TeamMates;
11} Employee;
12
13Employee* Employee_New(char* name, const int age)
14{
15    Employee* newEmp = (Employee*)calloc(1, sizeof(Employee));
16    memcpy(newEmp->Name, name, strlen(name)+1);
17    newEmp->Age = age;
18    newEmp->TeamMates = NULL;
19    newEmp->NumberOfTeamMates = 0;
20    return newEmp;
21}
22
23void Employee_Print(Employee* employee)
24{
25    printf("Name: %s\n", employee->Name);
26    printf("Age:  %d\n", employee->Age);
27    printf("Number of TeamMates: %d\n", employee->NumberOfTeamMates);
28    if(employee->NumberOfTeamMates > 0)
29    {
30        printf("\t");
31    }
32    for(int i = 0; i < employee->NumberOfTeamMates; i++)
33    {
34        Employee* teamMate = (Employee*) employee->TeamMates[i];
35        printf("%s, ", teamMate->Name);
36    }
37    printf("\n");
38}
39
40void Employee_AddTeamMate(Employee* employee, Employee* teamMate)
41{
42    employee->NumberOfTeamMates += 1;
43    employee->TeamMates = (struct Employee**)realloc(
44                              employee->TeamMates,
45                              sizeof(Employee*) * employee->NumberOfTeamMates);
46    employee->TeamMates[employee->NumberOfTeamMates-1] = (struct Employee *)teamMate;
47}
48
49int main(int argc, char* argv[])
50{
51    Employee *joe = Employee_New("Joe", 29);
52    Employee *tom = Employee_New("Tom", 32);
53    Employee *bill = Employee_New("Bill", 35);
54
55    Employee_Print(joe);
56
57    Employee_AddTeamMate(joe, tom);
58    Employee_AddTeamMate(joe, bill);
59
60    Employee_Print(joe);
61
62    return 0;
63}
64
65
66

Key points:

  • The Employee structure groups related fields in one allocation.

  • Functions such as Employee_New and Employee_Print act like methods by taking an Employee* argument.

  • The example uses dynamic allocation, so it also previews memory management issues handled later.

The systems-code-examples/strlen example is a small C string and memory-layout demonstration.

 1#include <stdio.h>
 2
 3int my_strlen(const char *str)
 4{
 5    int length = 0;
 6    while(*str != 0)
 7    {
 8        str++;
 9        length++;
10    }
11    return length;
12}
13
14
15int main(int argc, char* argv[])
16{
17
18    const char *str = "hello world";
19
20    int len = my_strlen(str);
21
22    printf("%d\n", len);
23
24    return 0;
25}

Key points:

  • C strings are byte arrays terminated by '\0'.

  • strlen() counts characters before the terminator, not the storage used by the array.

  • The example is useful for connecting string behavior to memory layout.

The systems-code-examples/parity example is a low-level bit manipulation example.

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <unistd.h>
 4#include <string.h>
 5#include <sys/stat.h>
 6#include <sys/types.h>
 7#include <fcntl.h>
 8
 9size_t parityWrite(
10    int fd0, int fd1, int fd2,
11    const void *buf0, const void *buf1,
12    size_t count)
13{
14    for(size_t i = 0; i < count; i++)
15    {
16        char byte0 = ((char*)buf0)[i];
17        char byte1 = ((char*)buf1)[i];
18        char parity = byte0 ^ byte1;
19        write(fd0, &byte0, sizeof(char));
20        write(fd1, &byte1, sizeof(char));
21        write(fd2, &parity, sizeof(char));
22    }
23    return count;
24}
25
26size_t parityRead(int fd0, int fd1, void *buf, size_t count)
27{
28    char *buff0 = (char*)malloc(count);
29    char *buff1 = (char*)malloc(count);
30    char *buff = (char*)buf;
31    read(fd0, buff0, count);
32    read(fd1, buff1, count);
33    for(size_t i = 0; i < count; i++)
34    {
35        buff[i] = buff0[i] ^ buff1[i];
36    }
37    return count;
38}
39
40int main(int argc, char** argv)
41{
42    int fd0 = open("f0", O_CREAT|O_TRUNC|O_RDWR, 0666);
43    int fd1 = open("f1", O_CREAT|O_TRUNC|O_RDWR, 0666);
44    int fd2 = open("f2", O_CREAT|O_TRUNC|O_RDWR, 0666);
45
46    const char* msg0 = "hello world\n";
47    const char* msg1 = "testing 123\n";
48
49    parityWrite(fd0,fd1,fd2,msg0,msg1,strlen(msg0)+1);
50
51    close(fd0);
52    close(fd1);
53    close(fd2);
54
55    unlink("f1");
56
57    fd0 = open("f0", O_RDWR, 0666);
58    fd2 = open("f2", O_RDWR, 0666);
59
60    size_t msgSize = sizeof(char)*strlen(msg0)+1;
61    char *buff = (char*)malloc(msgSize);
62
63    parityRead(fd0, fd2, buff, msgSize);
64
65    printf("f1 contents are = %s\n", buff);
66
67    close(fd0);
68    close(fd2);
69
70    free(buff);
71
72    unlink("f0");
73    unlink("f2");
74
75    return 0;
76}

Key points:

  • The code works directly with integer bits rather than higher-level data structures.

  • Bit operations are common in operating systems code for flags, masks, and packed state.

  • Small examples like this help make machine representation visible.