Breaking Down the Hello, world! in C πŸ’€


Simple programs often reveal fundamental concepts when analyzed in smaller parts. Therefore, consider the classic programming example: Hello, world!, written in C.

This simple example reveals several steps involved in building and running a C program, as well as exposing layers that are normally hidden when programming in higher-level languages.

hello-world.c

c
#include <stdio.h>

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

Line 0 β€” #include

Right on the first line written in a C program (#include <stdio.h>), you already need to dig deep to understand what's really happening.

TL;DR: #include <stdio.h> is not an instruction that the program executes β€” here, the include does something like: "copy the declarations contained in this header file and paste them into my source code."

Starting from the first character (yes, literally the first one), what is the role of the "#" before include? It's a clue for the preprocessor directives (which, by the way, I had absolutely no idea what it meant). The preprocessor takes part in the first stages of building a C program, before compilation itself β€” at this stage, the text of the source code is modified. In simplified terms, it looks mainly at lines that start with #, reads the directive, and reacts to it.

Here, the directive is the word include, which can be interpreted as the import of other languages β€” i.e., a way to include external code in your program. However, it is important to note that the implementation of include and import are technically different.

Next, <stdio.h>, which looks like a secret ancient code, is actually just a header file (which, in any case, is the gateway to that supposed secret ancient code). Headers are files that normally have the .h extension by convention; they contain things like: declarations, type definitions, macros, and other information that can be shared between C files. That is, during preprocessing, the contents of this file are inserted into the program's source code.

At this point a question arose: "aren't the functions declared in stdio.h literally copied and pasted into my source code?". The answer is: partially. The header content is inserted by the preprocessor, but it does not contain the implementation of those functions; it contains only their declarations. For example, in stdio.h there is a declaration similar to: int printf(const char *format, ...);. This line tells the compiler that a function called printf exists, what arguments it takes and what value it returns, but does not show how it works internally.

Then another question arises: if my code only has the declaration of printf, where is the code that actually executes that function? The implementation is present in the C standard libraries and is connected to the program in a later step, called linking.

The linker is the tool responsible for connecting the compiled code with the libraries and external resources it uses, resolving references such as calls to functions declared in headers but implemented in libraries.

  • header (stdio.h) β†’ informs that something exists and how to use it;
  • library β†’ contains the actual implementation;
  • linker β†’ connects the program with that implementation.

Up to this point the pipeline can be interpreted as follows:

text
file.c
   |
   v
pre-processor
   |
   v
processed source code
   |
   v
compiler
   |
   v
linker
   |
   v
executable

And before we move on to the main() function, it's worth remembering: all of this was necessary just to use printf and display the measly "Hello World" in the console.

  • "So that damn printf had to go through all of this to work?"
    • Yes (and in fact, that's just the tip of the iceberg).

Why does the main function usually return int?

The value returned by main is used to inform the operating system how the program finished its execution. By convention, the value 0 indicates success, while other values generally represent some type of error or special condition.

How do you run the program?

Naturally, that was my first question after following the code example from K&R (from the classic book The C Programming Language) and writing hello-world.c. Here I started to see that the source code is just one part of the whole process, and of course, the computer does not directly execute code written by humans. At some point, that code needs to be transformed into instructions that the processor can execute (represented by 0s and 1s). The one responsible for this work is the compiler.

Then another question arises: how do I install this so-called C compiler, and besides it, is there anything else? Luckily, for those using Linux, it is common for part of this infrastructure to already be present on the system, mainly because several parts of the Linux ecosystem itself depend on the C language. The reason is that the Linux kernel and many GNU tools are written in C, so a large part of the system depends on it.

Assuming it were necessary to install everything on a Linux system, it would be quite simple. Example:

  • Arch: sudo pacman -S base-devel
  • Debian: sudo apt install build-essential

Inside the base-devel packages for Arch distributions or build-essential for Debian distributions, there are several tools, e.g.: GCC (C/C++ compiler), binutils (linker, assembler and other tools), make and auxiliary build tools.

On a Windows system, the most common and practical options are:

  • Visual Studio Community (complete development package)
  • MSYS2 (development environment with GCC)
  • MinGW-w64 (GCC compiler for Windows)

With all the necessary tools to compile and run a program written in C, you just need to call the compiler and pass the source code β€” it will take care of everything else, and you can sleep in peace. Example:

  • GCC: gcc main.c -o main
  • Clang: clang main.c -o main

The idea is the same: the chain of steps explained in the section above is executed, the executable is generated, and now you just need to run the program with ./main on Linux or main.exe on Windows.

Enjoy the "Hello World" output in your terminal.

Simplified example of what exists on the computer to compile and run a C program:

text
Compiler
    |
    +-- gcc or clang

Headers
    |
    +-- stdio.h
    +-- stdlib.h
    +-- string.h
    +-- ...

Libraries
    |
    +-- printf implementation
    +-- malloc implementation
    +-- fopen implementation
    +-- ...

Recap

Pre-processor:

  • processes directives like #include;
  • inserts the contents of the headers.

Compiler:

  • checks the code;
  • transforms the C code into machine code.

Linker:

  • resolves external references;
  • connects libraries;
  • generates the final executable.

Operating system:

  • loads the executable into memory;
  • starts the process;
  • provides the resources necessary for execution.

Conclusion


So far we have explored, in a simplified way, how a C program is developed and executed. Observing this pipeline makes it evident how many layers of abstraction exist between the source code written by humans and the instructions actually executed by the processor. And, naturally, it is always possible to "go down one more level": observing the assembly generated by the compiler or even the final binary. From that point on, the next level of abstraction would be understanding how the processor itself was designed to interpret those instructions.

Learning the basics of C is valuable precisely because many concepts stop seeming like "arbitrary rules" and start making sense. Understanding these layers changes the way we see software: we realize that, through abstractions built by ourselves, we are capable of transforming ideas into instructions that a machine executes.

The day-to-day work of most programmers does not consist of analyzing assembly, directly manipulating memory, or implementing data structures from scratch. However, studying and experimenting with these concepts in practice strengthens the developer's mental model, making it clearer what happens behind the abstractions used on a daily basis.

The next time I write something in Python like products.append(product) or print("*" * 10), it will be more evident that these operations represent several decisions and implementations that the language hides from the programmer. In C, many of these details need to be explicitly described in the code; in modern languages, they still exist, but are abstracted away to allow for greater productivity.