What C is and how a program becomes a binary
C is a small compiled language that sits close to the machine: no garbage collector, no classes, no runtime. You write a text file, GCC turns it into machine code, and the operating system runs the result as a process. This lesson builds the smallest possible program and looks inside every step, on Linux in a Docker container.
From source to program
A C program starts as plain text with a .c suffix. Save the lines below as hello.c, then compile the file and run the binary the compiler writes. The -o hello option names that binary; without it GCC writes a.out.
#include <stdio.h>
int main(void)
{
printf("Hello, C!\n");
return 0;
}
main is the entry point: the C runtime calls it and its return value becomes the exit status of the process. printf writes to standard output. Compile with warnings enabled, then run the program from the same directory:
gcc -Wall -Wextra hello.c -o hello
./hello
Hello, C!
-Wall -Wextra asks for the warnings that catch most beginner mistakes. They cost nothing at run time, so keep them in every command of this course.
The four build stages
GCC is a driver, not one program. It runs the preprocessor, which handles #include and macros; the compiler, which turns C into assembly; the assembler, which turns assembly into an object file; and the linker, which joins object files and libraries into an executable. Each stage can be stopped and inspected with its own option.
#include <stdio.h>
int main(void)
{
int a = 6;
int b = 7;
printf("%d * %d = %d\n", a, b, a * b);
return 0;
}
-E stops after preprocessing, -S after compilation to assembly, -c after assembly. The last command links the object file on its own:
gcc -E stages.c -o stages.i
wc -l stages.i
gcc -S stages.c -o stages.s
wc -l stages.s
gcc -c stages.c -o stages.o
gcc stages.o -o stages
./stages
182 stages.i
37 stages.s
6 * 7 = 42
The preprocessed file is long because stdio.h pulls in declarations from other headers. The exact line counts depend on the library and compiler version, but the order of the stages never changes.
The default dialect is C23
GCC 15 compiles for C23 unless told otherwise, so __STDC_VERSION__ is 202311L. This course uses that default, and switches to -std=c17 only in a lesson about the older standard. The program below prints what the compiler selected.
#include <stdio.h>
int main(void)
{
printf("__STDC_VERSION__ = %ld\n", __STDC_VERSION__);
printf("__GNUC__ = %d\n", __GNUC__);
printf("hosted = %d\n", __STDC_HOSTED__);
return 0;
}
Build it twice, once with the default and once with -std=c17, and compare the first line of each run:
gcc --version | head -1
gcc -Wall -Wextra version.c -o version
./version
gcc -std=c17 -Wall -Wextra version.c -o version17
./version17
gcc (Alpine 15.2.0) 15.2.0
__STDC_VERSION__ = 202311
__GNUC__ = 15
hosted = 1
__STDC_VERSION__ = 201710
__GNUC__ = 15
hosted = 1
hosted = 1 means the full standard library is available, as opposed to a freestanding embedded build. The __GNUC__ value does not change with the dialect: it reports the compiler, not the language level.
Exit status
The value returned by main is handed to the parent process. The shell stores it in $?: zero means success, anything else is a failure code that a script can test. The program below returns 3.
#include <stdio.h>
int main(void)
{
printf("about to leave\n");
return 3;
}
Run the program, then ask the shell for the status of the last command:
gcc -Wall -Wextra status.c -o status
./status
echo "exit status: $?"
about to leave
exit status: 3
The message goes to standard output, the status travels separately, so a program can print a report and still fail. This is how the test scripts in lesson 63 decide whether a test passed.
Two mistakes that stop the build
The most common first failure is a misspelled entry point. The compiler accepts the function, the assembler accepts the object file, and the linker is the stage that reports the missing symbol:
#include <stdio.h>
int mian(void)
{
printf("this never runs\n");
return 0;
}
gcc -Wall -Wextra typo.c -o typo
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../lib/Scrt1.o: in function `_start_c':
/home/buildozer/aports/main/musl/src/musl-1.2.6/crt/crt1.c:18:(.text._start_c+0x1a): undefined reference to `main'
collect2: error: ld returned 1 exit status
The paths differ on other systems; the last line is the one to read. The second mistake is writing void main. GCC still builds the program but warns, because the standard requires int main:
#include <stdio.h>
void main(void)
{
printf("hello\n");
}
gcc -Wall -Wextra voidmain.c -o voidmain
./voidmain
voidmain.c:3:6: warning: return type of 'main' is not 'int' [-Wmain]
3 | void main(void)
| ^~~~
hello
Since C99, reaching the closing brace of main without a return is the same as returning 0, so the explicit return 0; is a habit rather than a requirement. Return the value anyway: it makes the exit status visible in the source.
Exercises
- Change the first program to print your name on a second line, then run it and check the exit status with echo $?.
- Compile stages.c with -O2, keep the assembly with -S, and find the line of assembly that loads the constant 6.
- Write a program that returns 0 when it prints one line and 1 when it prints two, then run both paths from the shell and compare $?.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which build stage joins object files and libraries into an executable?
Answer
The linker
The linker is the last stage: it resolves symbols such as
mainand pulls in the libraries the program uses. -
Which GCC option stops after producing assembly?
Answer
-S-Sstops after compilation and writes the assembly file;-cgoes one stage further and writes an object file. -
Which C standard does GCC 15 use when no
-stdoption is given?Answer
C23
GCC 15 defaults to C23, so
__STDC_VERSION__is202311Lunless another dialect is requested. -
What does the shell keep in
$??Answer
The exit status of the last command
$?is the exit status of the last command: zero for success and a non-zero value for failure. -
What does this program print?
#include <stdio.h> int main(void) { printf("%d\n", 6 * 7); return 0; }Answer
42
6 * 7is 42, and%dprints the value as a decimal integer. -
What does
printf("%ld\n", __STDC_VERSION__);print in a default GCC 15 build?Answer
202311
The default dialect is C23 and its
__STDC_VERSION__value is202311L. -
This program is built with
gcc -std=c17 -Wall -Wextra version.c -o version. What does it print?#include <stdio.h> int main(void) { printf("%ld\n", __STDC_VERSION__); return 0; }Answer
201710
-std=c17selects C17, whose__STDC_VERSION__value is201710L. -
This program is built as
statusand run with./status; echo "exit status: $?". What does the shell print?#include <stdio.h> int main(void) { printf("about to leave\n"); return 3; }Answer
about to leave / exit status: 3
The message goes to standard output, and the value returned by
mainbecomes the exit status 3. -
A source file defines
int mian(void)instead ofint main(void). Which build stage reports the missing entry point?Answer
The linker
The function is valid C, so the earlier stages succeed; the linker looks for
mainand reports an undefined reference. -
This program is built with
gcc -std=c17 -Wall -Wextra voidmain.c -o voidmain. What does GCC report?#include <stdio.h> void main(void) { printf("hello\n"); }Answer
warning: return type of 'main' is not 'int' [-Wmain]
GCC still builds the program but warns that the return type of
mainis notint.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev