The toy program. Here is toy.c. It opens a file, writes to it, prints a
message, compares a command-line argument with a word, and then waits 30
seconds so that we have time to inspect it.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
const char *greeting = "hello from the toy program";
FILE *f = fopen("/tmp/toy-notes.txt", "w");
if (f == NULL) return 1;
fprintf(f, "notes\n");
printf("%s\n", greeting);
if (argc > 1 && strcmp(argv[1], "open-sesame") == 0)
puts("the word matched");
else
puts("the word did not match");
if (argc > 2) return 0; /* second argument: skip the wait */
printf("pid %d, waiting 30 seconds\n", getpid());
fflush(stdout);
sleep(30);
fclose(f);
return 0;
}
Compile it and run it:
$ gcc -O0 -no-pie -fno-stack-protector -o toy toy.c
$ ./toy open-sesame
hello from the toy program
the word matched
pid 23, waiting 30 seconds
(-O0 turns off optimization so the machine code stays close to the C.
-no-pie makes the program load at a fixed address, which keeps addresses
easy to read. Later modules explain why that matters.)
You can compile the toy yourself in the challenge environment and follow along.
What is a process? A program is a file on disk. A process is that
program while it is running: the file's code loaded into memory, plus
everything the operating system tracks for it. You can run one program twice
and get two separate processes.
What is a PID? Linux gives every process a number, its PID (process
ID). Tools use the PID to say which process they mean. In the toy's output
above, pid 23 is the number getpid() returned. Every process also records
the PID of the process that started it, its PPID (parent PID). When your
shell runs ./toy, the shell is the parent and toy is the child.
What is a file descriptor? When a process opens a file, Linux does not
hand back the file. It hands back a small number, a file descriptor (fd).
The process uses that number in later calls ("write these bytes to fd 3"). A
new process starts with three already open:
fd 0 standard input where the program reads from (normally your keyboard)
fd 1 standard output where printf and puts write (normally your screen)
fd 2 standard error where error messages go
fd 3+ files the program opened itself
The toy opened /tmp/toy-notes.txt, so it got the next free number, 3.
What is a memory map? A process sees memory as one long list of numbered
bytes. The number of a byte is its address, written in hex. The process
only owns some ranges of addresses. Each owned range is a mapping. A
mapping has a start, an end, and permissions: r (read), w (write),
x (execute, meaning the CPU may run it as code). The picture below shows the
usual layout (high addresses at the top):
high addresses
[stack] local variables, function return addresses rw-
libc.so.6, ... shared libraries (printf, puts, strcmp live here)
their code is r-x, their data rw-
[heap] memory from malloc rw-
program: .data global variables that have a value rw-
program: .rodata constants and string literals r--
program: .text the program's instructions r-x
low addresses
Note what is not allowed: no mapping is both writable and executable in a
normal program. Later modules explain why that matters.
What is /proc? /proc looks like a folder, but it is not stored on the
disk. It is a window the kernel (the core of the operating system) provides
into itself. For every running process there is a folder /proc/<pid>/.
Reading a file in it asks the kernel a question and gets the answer as text.
The ones you need:
/proc/<pid>/maps the memory map
/proc/<pid>/fd/ one entry per open file descriptor
/proc/<pid>/exe a link to the program file being run
/proc/<pid>/cmdline the command line the process was started with
/proc/self/... "my own process": whichever program reads it sees itself
Because /proc/self means "whoever is reading", cat /proc/self/maps shows
the map of cat, not of your shell. To look at another process you must use
its real PID.