How to Compile and Run C Programs on Linux – GCC Basics

Linux is C’s home turf — the kernel, the shell, and most of the tools you’re using were written in it — and compiling C there is a two-command job once you know the pattern. This guide takes you from a .c file to a running program on any Linux system (a desktop, a cloud server over SSH, WSL on Windows, or a Docker container — the output below was captured on Ubuntu 24.04 with gcc 13.3), and walks through the four mistakes every beginner makes in their first week.

Step 0 — Make Sure GCC Is Installed

gcc --version

If you get gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 or similar, you’re set. If you get command not found, install the whole C toolchain in one go:

sudo apt install build-essential

(That’s Ubuntu/Debian — full walkthrough with other distros in How to Install GCC on Ubuntu.) build-essential brings gcc, the linker, Make, and the standard library headers — everything this guide uses.

Step 1 — Compile and Run Your First Program

Save this as hello.c:

#include <stdio.h>

int main(void)
{
    printf("Hello from Linux!\n");
    return 0;
}

Compile it with no options at all and see what appears:

$ gcc hello.c
$ ls
a.out  hello.c
$ ./a.out
Hello from Linux!

Two things just happened that deserve a sentence each. First, GCC’s default output name is a.out (“assembler output” — a name older than Linux itself). Second, you must run it as ./a.out, not a.out. Forget the ./ and you’ll hit the most common first-day error:

$ a.out
bash: a.out: command not found

The shell only searches its PATH directories for commands — not your current directory. ./ says “run the one right here.”

Step 2 — Name Your Output with -o

$ gcc -o hello hello.c
$ ./hello
Hello from Linux!

-o hello names the executable. Careful with argument order on this one — gcc -o hello.c hello would tell GCC to overwrite your source file with the output. The safe habit: -o is always followed immediately by the program name.

Step 3 — Always Compile with Warnings On

This is the single highest-value habit in C. Here’s a program with a real bug — total is printed without ever being given a value:

int total;
printf("%d\n", total);

Plain gcc compiles it silently and it prints garbage. With warnings on:

$ gcc -Wall -o buggy buggy.c
buggy.c:6:5: warning: 'total' is used uninitialized [-Wuninitialized]
    6 |     printf("%d\n", total);
      |     ^~~~~~~~~~~~~~~~~~~~~

The compiler found the bug before the program ever ran. Make this your default compile command and never look back:

gcc -Wall -Wextra -g -o program program.c

(-g adds debug info so GDB and Valgrind can show you file and line numbers — it costs nothing while learning.)

Step 4 — The Math Library Trap (-lm)

Sooner or later you’ll use sqrt() or pow() from <math.h>, and on Linux this happens:

$ gcc -o roots roots.c
/usr/bin/ld: /tmp/cceTEy0m.o: in function `main':
roots.c:(.text+0x14): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status

Note who’s complaining: ld, the linker, not the compiler. Your code is fine — the header declared sqrt, but the compiled math library containing its actual code was never linked in. On Linux the math library (libm) is separate from the C library, and you attach it with -lm at the end of the command:

$ gcc -o roots roots.c -lm
$ ./roots
sqrt(2.0) = 1.414214

Any undefined reference error means the same thing: some compiled code is missing from the link — a library you didn’t -l, or (next section) a source file you forgot to list.

Step 5 — Compiling Multiple Files

Real programs span files. Pass all the .c files together (headers are never compiled directly — #include pulls them in):

$ gcc -Wall -o calc main.c utils.c
$ ./calc
3 + 4 = 7
3 * 4 = 12

The moment a project has more than two or three files, retyping this stops scaling — that’s precisely what build tools solve, and our Makefile tutorial automates this exact calculator project (then the CMake tutorial modernizes it).

What Exactly Did You Build?

$ file hello
hello: ELF 64-bit LSB pie executable, ARM aarch64, ... dynamically linked ...

ELF is Linux’s native executable format, compiled for this specific machine’s CPU. That’s why you can’t copy the binary to a Mac (which uses Mach-O — see C setup on Mac) or Windows and run it: in C, you share the source and each machine compiles its own binary.

Common Issues

Error Cause & Fix
gcc: command not found Toolchain not installed — sudo apt install build-essential
bash: a.out: command not found Missing ./ — run ./a.out
undefined reference to 'sqrt' Math library not linked — add -lm at the end of the command
undefined reference to one of your functions You compiled only one .c file — list them all: gcc main.c utils.c
stdio.h: No such file or directory Headers missing — build-essential installs them
Permission denied running your program Usually a filesystem mounted noexec (USB drives, some shared folders) — build in your home directory

What’s Next

You now have the full compile-run-fix loop. Level it up in the order the pros did: warnings always on, GDB when the program misbehaves, Valgrind before you trust anything with malloc, and Make once the project grows. Wondering about Clang instead of GCC? We compared them side by side.


As an Amazon Associate we earn from qualifying purchases.

Recommended Book

C and Unix grew up together, and The C Programming Language by Kernighan & Ritchie (Amazon.com) is the book that taught the world both — every example compiles exactly as shown with the gcc commands you just learned.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>