Software development, step by step: from source code to a shared project.
How source code becomes a running program, the tools that help you write and debug it, how make rebuilds only what changed, and how Git keeps the history and shares it.
10 chapters · 21 pages≈ 148 mininteractive: the C pipeline, demo terminals for gcc, make and git
Your progress
Start with compiled and interpreted languages.
Follow the chapters in order or pick one on the map.
From the source to the build, the debugger and Git.
Current chapterDone
AI
Study material generated with AI (Claude) from the slides of ISO lecture 07, to be reviewed by the lecturers. It may contain mistakes; for the exam, the reference is the lecture and its slides. Found a mistake? Open an issue on the course repository.
About this material
How it was made
Text, figures and the simulators were generated with Claude (Anthropic) from the slides of lecture 07 – Software Development, following the structure of the interactive DEEA modules.
What was checked
The outputs shown come from the slides. The make simulator follows the rules of GNU Make (modification times); the git terminal models status, diff, add, commit, log, branches, merge (with conflicts), fetch, pull and push, not every case of Git.
Mistakes
If you find one, open an issue with the page and the sentence. Version: November 2026.
Sources and contributors
Slides: USO Team, Copyright (C) 2026 Wyliodrin & Politehnica Bucharest, CC-BY-SA 4.0. Bibliography: the sources listed in the slides of the lecture.
The processor only runs machine code. When your source is translated (before it runs, or while it runs) decides how fast the program is, when you see its mistakes, and what you give to the people who use it.
what you write: source code, text
for (int i = 0; i < 3; i++) {
printf("Hello, %d!\n", i);
}
what the CPU runs: machine code, bytes (x86-64)
c7 45 fc 00 00 00 00 # i = 0
eb 1d # jump to the test
...
e8 00 00 00 00 # call printf
83 45 fc 01 # i++
83 7d fc 02 # i <= 2 ?
7e dd # if yes, jump back
every processor family has its own instruction set: these bytes mean nothing to an ARM64 or a RISC-V CPU
so the source must be translated to the machine code of this CPU: who does it, and when?
Three answers
you write
before it runs
▶ run
every time it runs
CompiledC, C++, Rust, Go
hello.c
gcc compiler→hello machine code
the CPU runs it directly
Bytecode + VMJava, C#, Python, JavaScript
Hello.java
javac→.class bytecode
java VM + JIT→CPU
Interpretedbash, sh
hello.sh
nothing
bash, line by line→CPU
A compiler is a translator who translates the whole book once, before anyone reads it. An interpreter translates sentence by sentence, every time someone reads it.
$gcc hello.c -o hello # once$./hello # as many times as you wantHello, 0!$file hellohello: ELF 64-bit LSB pie executable, x86-64
The executable is machine code: it runs without gcc, but only on x86-64 Linux.
The script is only text: the CPU runs bash, and bash does what the script says.
Try it
When is the mistake found?
The clearest difference shows up when the code has a mistake. A compiler reads all of the file before anything runs; an interpreter finds a mistake only when it reaches that line.
Line 3 has
C (gcc)
Python (python3)
bash
a typo ())
error, nothing runs
SyntaxError, nothing runs
lines 1, 2 run, then the error
a misspelled name
error, nothing runs
lines 1, 2 run, then NameError
lines 1, 2 run, an error, then line 4
a change, not rebuilt
runs the old code
runs the new code
runs the new code
Why it matters
A compiled program cannot crash on a typo: it would not have been built. A script can stop halfway, after it has already done part of its work (deleted some files, written half a report). Test every branch of a script, not only the first lines.
The middle way
Bytecode and virtual machines
javac compiles once, to bytecode: machine code for an imaginary CPU
the virtual machine (java) runs the bytecode on any OS and CPU it is installed on: write once, run anywhere
JIT (just in time): the VM watches which code runs often and compiles it to real machine code, while the program runs
$javac Hello.java$file Hello.classHello.class: compiled Java class data$java HelloHello, 0!
Python and JavaScript are compiled too
you run the source, python3 total.py, but python3 first compiles all of the file to bytecode, then its VM runs it
that is why a SyntaxError anywhere stops the script before its first line runs
the bytecode of imported modules is kept in __pycache__/, so the next run starts faster
node, deno and Chrome share the V8 engine, which JIT-compiles the code that runs often
Only shells really read and run one command at a time.
The big picture
Compiled vs. interpreted
Compiled (C, C++, Rust, Go)
Bytecode + VM (Java, C#, Python, JavaScript)
Interpreted (bash, sh)
translated
once, before it runs
to bytecode, then by the VM (and the JIT)
every time, line by line
speed
fast
slower; the JIT helps
slow
mistakes found
syntax, types, names: before it runs
syntax before it runs, the rest when the line runs
when the line runs
you changed the code
compile again, then run
run it
run it
another CPU or OS
compile again for it
runs where the VM is installed
runs where the interpreter is
you give users
the executable
the bytecode or the source
the source
A language is not compiled or interpreted
The program that runs it is: C can be compiled by gcc or interpreted by picoc; Python can be run by python3 (bytecode) or compiled to an executable by nuitka.
Check yourself
You change one line of a C program and run ./app. What happens?
Check yourself
A Python script prints two lines, then stops with NameError on line 3. Was line 3 compiled?
Integrated Development Environment
What is an IDE?
Why it matters
An IDE puts the editor, the build, the debugger and git in one window. Knowing which part does what lets you use any of them, or build the same setup from a plain editor.
Part
What it does
build
runs the compiler or make, shows the errors in the code
run
one key, the output in a panel
code intelligence
the type of a variable, where a function is defined, completion, refactoring
debugger
stop on a line, look at the variables
project
the files of the program and how to build them
version control
the changes, commit, push, inside the IDE
Xcode: the editor in the middle, the project on the left, the debugger below
How? With language servers and debug adapters: the next chapters.
LSP
The Language Server Protocol
The problem
to understand C, an editor must parse it, follow the #includes and know the types: the work of a compiler
5 editors × 5 languages = 25 plugins, each with its own C parser
with one protocol, 5 + 5 = 10 programs: every editor talks to every server
LSP (Microsoft, 2016)
The editor asks, the language server answers: clangd for C/C++, rust-analyzer, pyright, gopls ...
How it starts
you run hx main.c
the editor sees a C file and startsclangd: fork + exec, a child process
two pipes connect them: the code and questions to its stdin, answers and errors from its stdout
you type, the editor sends the changes and shows the answers
clangd crashes? the editor keeps working
A conversation
Three kinds of messages, all JSON: a notification (no answer: I opened main.c), a request with an id (what is under the cursor?) and the response with the same id.
hover: the type of sum and the header it comes from
completion: the server knows every file of the project
Also from the server: the errors while you type (diagnostics), go to definition, rename in every file.
Instead of printf
What is a debugger?
Why it matters
printf shows what you guessed to print. A debugger stops the program where you want and shows everything: variables, the call stack, memory. Most bugs are found faster this way.
breakpoints: stop at a line or a function
step: run one line at a time
inspect: variables, memory, the call stack
watchpoints: stop when a variable changes
change a variable and continue
Debuggers: gdb (GNU), lldb (LLVM), the Visual Studio debugger.
How it works
gdb starts the program itself: fork + exec, app is its child, stopped before its first instruction
the ptrace system call: gdb asks the kernel to read and write the memory and registers of app, to stop and continue it
app reaches the breakpoint: the kernel stops it and tells gdb
$gdb-q ./appReading symbols from ./app...(gdb) break sumBreakpoint 1 at 0x1188: file lib.c, line 3.(gdb) runBreakpoint 1, sum (n=10) at lib.c:33 int s = 0;(gdb) print n$1 = 10(gdb) next4 for (int i = 1; i <= n; i++) {
gdb -tui (or Ctrl+XA) shows the source above the commands: B+ a breakpoint, > the next line to run.
Common mistake
Debugging a program built without -g
Without debug information the executable is only machine code: gdb cannot show lines or variable names.
Instead: compile with gcc -g (a debug build); a release build usually leaves it out. Rust (cargo build), Go and javac -g add it too.
DAP
The Debug Adapter Protocol
the same problem as LSP: every debugger has its own commands (break in gdb, breakpoint set in lldb)
DAP (Microsoft, 2016): the editor asks, the debug adapter answers
the editor starts the adapter (e.g. lldb-dap), two pipes connect them, as for LSP
the adapter starts app and controls it with ptrace, like gdb
VSCodium: the breakpoint, the stopped line, the variables and the call stack, all from DAP
Language
Debug adapter
C, C++, Rust
lldb-dap, CodeLLDB, gdb -i=dap (gdb 14+)
Python
debugpy
Go
dlv dap (Delve)
JavaScript, TypeScript
js-debug
Four steps
From hello.c to an executable
Why it matters
Every build error comes from one of the four steps: a missing header from the preprocessor, a type error from the compiler, an undefined reference from the linker. Know the step, and the message makes sense.
Stop after any step
Stop after
Command
You get
Look at it with
1. preprocessor
gcc -E hello.c -o hello.i
hello.i, C text
wc -l, tail
2. compiler
gcc -S hello.c
hello.s, assembly
cat
3. assembler
gcc -c hello.c
hello.o, an object file
nm, objdump -d, file
4. linker
gcc hello.o -o hello
hello, the executable
file, ldd, ./hello
gcc -save-temps hello.c -o hello keeps all of them: hello.i, hello.s, hello.o.
To read hello.s: -fverbose-asm writes each line of C as a comment above its instructions; -masm=intel writes the Intel syntax of the processor manuals instead of AT&T. For an object built with gcc -g, objdump -d -S -M intel shows each line of C above its machine code.
Static and shared
Libraries
A library is a file of compiled functions, ready for other programs: printf is in the C library, sqrt in the math library. The two kinds differ in when the code of the library joins the program: when it is linked (static) or every time it starts (shared).
Static library libsum.a
1. build the library
lib.osumar rcslibsum.aan archive of .o files
2. link the program
main.oU sumgcc -L. -lsumappmain + sum, copied in
3. run
apphas everythingneeds nothing else
a new libsum.a reaches app only through a new link
Shared library libsum.so
1. build the library
lib.osum, -fPICgcc -sharedlibsum.soa separate file
2. link the program
main.oU sumgcc -L. -lsumappmain + needs libsum.so
3. run, every time
appmainthe loader openslibsum.soone copy for all
a new libsum.so changes app at its next start
Static (libNAME.a)
Shared (libNAME.so, the default)
the linker
copies the functions into the executable
only writes down that the executable needs the library
when the program starts
nothing to load
the loader (ld-linux-x86-64.so.2) finds and loads the .so
a new version of the library
link every program again
every program uses it at its next start
the library is not installed
the program still runs
error while loading shared libraries
size
every program has its own copy (hello-static: 636K)
one copy for all (hello: 12K)
The commands
Command
What it does
ar rcs libsum.a lib.o
a static library from lib.o; ar t libsum.a lists what is inside
gcc -fPIC -c lib.c, then gcc -shared lib.o -o libsum.so
a shared library (-fPIC: code that works at any address)
gcc main.o print.o -L. -lsum -o app
link with libsum: -L. looks in this folder; .so is preferred to .a
ldd app
the shared libraries app needs, and where the loader finds them
compile each .c to its own .o, then link them into app
after a change, compile again only the file that changed, then link again
in a big project this saves minutes on every build; remembering what changed is the job of make
Which step complains?
The first words of the message tell the step: file.c:line:column: comes from the preprocessor or the compiler, /usr/bin/ld: from the linker.
1 · preprocessor
main.c:1:10: fatal error: lib.h: No such file or directory
The #included file is not found: a wrong name, or a folder missing (-I dir).
2 · compiler
main.c:6:17: error: implicit declaration of function 'sum' [-Wimplicit-function-declaration]
sum is used, but nothing declared it: the #include "lib.h" is missing. (An error since GCC 14.)
4 · linker
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x13): undefined reference to `sum'
Every file compiled, but no object file definessum: lib.o is missing from the command.
4 · linker
/usr/bin/ld: calc.o: in function `main':
calc.c:(.text+0x2a): undefined reference to `sqrt'
The function is in a library that is not linked: add -lm (the math library).
Check yourself
gcc main.o print.o -o app fails with undefined reference to 'sum'. Which step complains?
Rules: target, prerequisites, recipe
A Makefile
Why it matters
make runs only the commands that are needed, in the right order. Almost every C project (and these slides) is built with a Makefile; its mistakes, like a forgotten header, give programs that run with old code.
It never looks inside the files: touch (a new modification time) is enough to rebuild. make --debug=v prints every decision: Considering target file ..., Prerequisite 'print.c' is newer than target 'print.o', Must remake target ....
From four rules to %.o: %.c
A shorter Makefile
The three .o rules are the same, only the names change. Three steps make the Makefile shorter; make runs exactly the same commands.
CC: the compiler, CFLAGS: its options (-Wall all warnings, -g debug information)
change the compiler or the options in one place
make prints the commands after filling in the variables: gcc -Wall -g -c main.c -o main.o
The Makefile is not a prerequisite
Change CFLAGS and make says up to date: the .o files are newer than the .c files. make -B (or make clean, then make) rebuilds everything with the new options.
clean, all, run, .PHONY
Special targets
~/project/Makefile
CC = gcc
CFLAGS = -Wall -g
.PHONY: all build run cleanall: buildbuild: apprun: build./appapp: main.o lib.o print.o$(CC)$^ -o $@%.o:%.c$(CC)$(CFLAGS) -c $< -o $@clean:
rm -f *.o app
clean, all, build, run are targets that are not files
all: build and build: app have no recipe: they only name what to build
run: build first builds, then runs ./app
if a file named clean existed, make clean would say up to date and delete nothing
.PHONY tells make they are not files: their recipe always runs
all, clean, install, check are GNU conventions
make clean && make builds everything from scratch, like make -B.
-MMD: gcc writes the headers each file includes into a .d file, and the Makefile includes them. (Not in the demo terminal.)
The same bug, in pictures
Pick a Makefile, change lib.h and run make: the boxes show which files are rebuilt and with which MAX.
Save points for your code
What is Git?
Why it matters
Every lab and homework is handed in through a Git repository, and every software team works with one. Git keeps every version, shows who changed what, and lets several people change the same code.
a version control system: keeps the history of the files
go back to any version, see who changed what and why
distributed: every copy has the whole history
written in 2005 by Linus Torvalds, for Linux
the folder with the files and their history is a repository (repo)
a commit is a snapshot of the files, with a message and an author: a save point, like in a game
every commit points to its parent: the chain is the history (git log --oneline)
a branch is a name for a commit that moves forward with every new commit; HEAD is the branch you are on
Four places
Place
What is there
Command that moves a change to the next place
working directory
the files you edit
git add picks what goes in the commit
staging area
the next commit
git commit saves it in the local repository
local repository
the history, on your computer
git push sends it to the remote
remote repository
GitHub, GitLab ..., shared
git pull brings the others' commits
Commit is not push
git commit saves only on your computer: your colleagues (and your teacher) do not see it. git status says Your branch is ahead of 'origin/main' by 1 commit until you git push.
Try it
Git in a terminal
A clone of a project from GitHub, in ~/project. Type the git commands; the panel next to the terminal shows where your changes are and how the history grows. Follow the missions below the terminal.
Git cheat sheet
A few commands
Command
What it does
git clone <url>
copy a remote repository
git init
a new repository, here
git status
what changed, what to do next
git add <file>
stage a change
git commit -m "..."
save a snapshot
git push
send the commits
git pull
get the others' commits
git log --oneline
the history
git diff
the changes not staged yet
git branch
list the branches
git switch -c <name>
a new branch, go to it
git switch <name>
go to a branch
git merge <name>
merge it into this branch
Check yourself
You committed your homework but the teacher does not see it. What is missing?
Configure the project
Editor + plugins = IDE
the language server understands the code, the editor asks it through LSP
the debug adapter drives the debugger, through DAP
a task runs the build, the editor reads the errors from its output
the editor runs git and shows the changes next to the code
F5: the make task builds app, then the debugger starts it. .vscode/ is a hidden folder in the root of the project: commit it, and everyone gets the same build and debug setup.
Common mistake
Trusting every folder
Opening a project can run code: the language server and tasks the folder's settings name, and for Rust, rust-analyzer builds the project and runs its build.rs as soon as the folder opens.
Instead: open unknown code in restricted mode (do not trust the folder): you can read and edit it, nothing from it runs.
Quick recap
The ideas, in short
One idea per card; the title is the idea, the text says why it matters in practice. Recap shows all the cards, Flashcard one at a time.
01 · Languages
The CPU runs only machine code
click / Space
01 · Languages
The CPU runs only machine code
Something must translate the source: a compiler before it runs, or an interpreter / VM while it runs.