Skip to content
Courses

From Executable to Process

How Programs WorkLesson 1 of 760 minutesLessonBuilds on: Code Investigation LabNo lab next

People use the word program to mean source code, an executable file, or a running process. When we investigate what a program does, we need to be clear about which one we mean. Reading a file on disk tells us something different from inspecting a process that has started running.

  • Source code describes behavior in a language such as C.
  • An executable file stores information and machine code in a format Linux knows how to load.
  • A process is one running instance with an identity, memory, execution state, and operating-system resources.

An executable can sit on disk without being run. Starting it creates a process with its own lifetime and state. That process can be executing instructions, waiting for input, or sleeping, as the example below will show.

The executable contains code and information needed to load it. To see what happens during a particular run, we inspect the process: its memory, output, open files, and interactions with the operating system.

We’ll use sleep, a utility that waits for a specified amount of time before exiting. That gives us time to inspect its process while it waits. First, find the executable your shell would select:

Terminal window
command -v sleep

On the course’s Ubuntu VM, this will normally print /usr/bin/sleep. command -v asks the shell which command it would run for that name, so we can use the path it reports instead of guessing where the executable is installed.

Store the result in a shell variable so later commands use the exact path you found:

Terminal window
sleep_path="$(command -v sleep)"
printf '%s\n' "$sleep_path"

Now inspect the file itself:

Terminal window
file "$sleep_path"
stat "$sleep_path"

file should identify an ELF executable or shared object for your machine’s architecture. stat reports its size, ownership, permissions, and timestamps. So far, we’ve inspected the executable without starting a sleep process.

Run sleep for two minutes and place it in the background:

Terminal window
sleep 120 &

The & starts the command in the background, so the shell returns your prompt while sleep is still running. The shell prints a job number and a process ID, usually shortened to PID.

The shell also stores the PID of the most recently started background process in $!. Save it immediately:

Terminal window
sleep_pid=$!
printf 'PID: %s\n' "$sleep_pid"

The variable is named sleep_pid; $sleep_pid asks the shell to substitute its stored value. Quoting "$sleep_pid" keeps that value as a single argument.

Inspect the process it created:

Terminal window
ps -p "$sleep_pid" -o pid,ppid,stat,comm,args

The headings mean:

Field What it tells us
PID The identity Linux assigned this process
PPID The process ID of its parent, normally your shell here
STAT A compact description of its current process state
COMMAND or COMM The short command name
COMMAND or ARGS The command and arguments used to start it

Linux assigns PIDs when processes are created. Your numbers will differ from those in another run; use the ones you recorded in this shell.

What happened between the command and the process?

Section titled “What happened between the command and the process?”

Starting a process involves several steps. We’ll examine them more closely throughout this module. For now, use this outline:

  1. The shell interpreted the command line and found the sleep executable.
  2. The shell asked the Linux kernel to create and start a process.
  3. Linux inspected the executable format.
  4. The loader mapped the required code and data into the new process’s virtual address space.
  5. Linux prepared the arguments and initial execution state.
  6. The processor began executing at the program’s designated entry point.

This outline gives us more specific questions to investigate. Which parts of the executable were mapped into memory? Where did execution begin? How did the argument 120 reach the program? The next lessons examine those steps.

For a dynamically linked program, the startup path also includes loading the libraries it needs:

A dynamically linked executable travels through the shell, kernel, loader, entry point, and runtime before main
Shellparse commandresolve path
Kernelexecvevalidate ELF + map LOAD
Dynamic loadermap librariesresolve relocations
Entry point_startruntime setup
C programmainauthor code

The shell, kernel, and dynamic loader each handle part of this work. execve replaces the calling process’s loaded program. An interactive shell usually creates a child process first, then has that child load the executable so the shell can keep running. Static executables don’t need the dynamic-loader step shown here.

Linux exposes information about running processes through /proc, and each process gets a directory named after its PID.

List the process directory:

Terminal window
ls -ld "/proc/$sleep_pid"

Now inspect the exe entry:

Terminal window
ls -l "/proc/$sleep_pid/exe"
readlink "/proc/$sleep_pid/exe"

The exe entry is a symbolic link to the executable loaded for this process. readlink prints its target. This should identify the same file that command -v sleep found. The paths may look different if one uses a symbolic link, such as /bin pointing to /usr/bin.

This gives us an evidence chain:

command name → executable path → PID → /proc/PID/exe → executable path

The proc_pid_exe manual describes this link and its limitations, including what it can show after an executable has been unlinked.

Now start a second process from the same executable:

Terminal window
sleep 120 &
second_pid=$!
printf 'First PID: %s\nSecond PID: %s\n' "$sleep_pid" "$second_pid"

Then inspect them both together:

Terminal window
ps -p "$sleep_pid,$second_pid" -o pid,ppid,stat,comm,args

Both processes use the same executable and received the same argument, but each has a different PID. Each also has its own /proc directory and execution state.

Run readlink on both of them:

Terminal window
readlink "/proc/$sleep_pid/exe"
readlink "/proc/$second_pid/exe"

Both links should point to the same executable. Together with the different PIDs, this shows that one executable file can be used by two separate processes.

The processes will exit on their own after two minutes. If they’re still running, stop them now so we can check what disappears:

Terminal window
kill "$sleep_pid" "$second_pid"
wait "$sleep_pid" "$second_pid"

kill sends a termination signal by default. wait waits for these background processes to finish and collects their exit status. A Terminated message is expected here because we asked the processes to stop.

Confirm that neither process remains:

Terminal window
ps -p "$sleep_pid,$second_pid" -o pid,stat,comm,args

Once both processes have ended and the shell has collected their status, ps should show only the headings. Their /proc directories should be gone too.

After cleanup, the two PIDs should be gone while the executable file remains. That file can start another process. If a PID still appears, verify its command before stopping it; do not assume a reused PID is your old process.

  • A saved PID has no ps row: the process ended. Start a new controlled sleep, replace the variable, and verify it before you continue.
  • readlink reports a missing path: check the process still exists, and check the PID variable actually belongs to it.
  • Two terminals show different job state: go back to the shell that launched the background process, and use ps for the system-visible identity.
  • You aren’t certain the cleanup worked: verify each PID and command individually, send TERM, wait, and then prove both rows are gone.

You now have evidence for the distinction we started with: the executable file remained in place while two processes started and stopped. Their PIDs identified separate runs, and their /proc links connected both runs to the same file.

Next, Reading an ELF File examines the information Linux uses to load that executable.

Knowledge check

Question 1 of 2

Two PIDs resolve /proc/PID/exe to the same file. What does that establish?