Search code examples
cgcccompilation

Breaking down the C compilation process into preprocessing, compilation, assembling, and linking step in terms of their input/output


When I studied the the compilation process steps of the C programming language, I've tried to break down each step (preprocessing, compilation, assembling, and linking) in terms of input/output in order to grasp what is going on under the hood.

Unfortunately, I failed in doing so due to my little knowledge in handling the options of the gcc compiler (plese, let us stick with gcc as it is the available compiler on my Linux machine). I even managed the output for the preprocessing step, but I failed for the others.

I really appreciate any help in this sense.

I do not want a comprehensive explanation of each step. It is already available on internet. I want to see the their input/ouput and which gcc option I need to pass in order to get this done.


Solution

  • Let us adopt the simple "hello world!" C program named main.c

    #include <stdio.h>
    int main() {
       printf("Hello, World!");
       return 0;
    }
    

    Preprocessor

    enter image description here

    • The input file for this stage is *.c file.
    • It performs the following tasks:
      • Removing comments : It removes all the comments.
      • Line Splicing (\): The backslash \ is used for line splicing, allowing you to break long lines of code into multiple lines for readability, are concatenated to form a single line.
      • Header files inclusion: For example, if the directive #include <stdio.h> is available in the program, then the preprocessor interprets the directive and replaces this directive with the content of the /usr/include/stdio.h file.
      • Macro expansion: A macro is a preprocessor directive that allows you to define a symbolic name or identifier for a sequence of code. They are used to create code snippets that can be easily reused and to introduce simple text substitutions. The most common use of macros in C is through the #define directive, which defines a macro. They can be constant macros, e.g., #define BUFFER_SIZE 1024, or function-like macros, e.g., #define MAX(a, b) ((a) > (b) ? (a) : (b)).
      • Resolve the conditional compilation directives: Using special preprocessing directives, you can include or exclude parts of the program according to various conditions. For example, #ifdef DEBUG printf("Debugging is enabled.\n"); #endif. Other conditional compilations are #ifndef, #if, #elif, #else. The preprocessor evaluates these conditions and determines whether the enclosed code should be included in the preprocessed output file.
      • Line Control: The C preprocessor informs the C compiler of the location in your source code where each token came from. A token in C can be defined as the smallest individual element of the C programming language that is meaningful to the compiler. It is the basic component of a C program. They can be Keywords (double, if, while, return, ...); Identifiers (variable and function names); Constants (const int c_var = 20;); Strings; Special Symbols ([], (), {}, ,, #, ...); Operators (unary, binary, and ternary operators);
    • In nutshell, the preprocessor expands the code.
    • The output file is *.i or preprocessed file. This preprocessed file is the so-called translation unit (or more casually a compilation unit) and is the ultimate input to a C or C++ compiler from which an object file is generated. Our static functions are visible only within this file.
      gcc -E main.c -o main.i
    
    • The option -E Stop after the preprocessing stage; do not run the compiler proper. The output is in the form of preprocessed source code, which is sent to the standard output (or to a file with the -o option).

    Alternatively, you can directly use cpp (C preprocessor) to obtain the preprocessed file:

      cpp main.c -o main2.i
    

    Note that both file are exactly the same

      ❯ cmp main.i main2.i && echo "Files are equal" || echo "Files are not equal"
      Files are equal
    

    The output is

    # 0 "main.c"
    # 0 "<built-in>"
    # 0 "<command-line>"
    # 1 "/usr/include/stdc-predef.h" 1 3 4
    # 0 "<command-line>" 2
    # 1 "main.c"
    # 1 "/usr/include/stdio.h" 1 3 4
    # 27 "/usr/include/stdio.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
    # 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
    # 1 "/usr/include/features.h" 1 3 4
    # 392 "/usr/include/features.h" 3 4
    # 1 "/usr/include/features-time64.h" 1 3 4
    # 20 "/usr/include/features-time64.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
    # 21 "/usr/include/features-time64.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4
    # 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
    # 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4
    # 22 "/usr/include/features-time64.h" 2 3 4
    # 393 "/usr/include/features.h" 2 3 4
    # 486 "/usr/include/features.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
    # 559 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
    # 560 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
    # 561 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
    # 487 "/usr/include/features.h" 2 3 4
    # 510 "/usr/include/features.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
    # 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
    # 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
    # 511 "/usr/include/features.h" 2 3 4
    # 34 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 2 3 4
    # 28 "/usr/include/stdio.h" 2 3 4
    
    
    
    
    
    # 1 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h" 1 3 4
    # 209 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h" 3 4
    
    # 209 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h" 3 4
    typedef long unsigned int size_t;
    # 34 "/usr/include/stdio.h" 2 3 4
    
    
    # 1 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stdarg.h" 1 3 4
    # 40 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stdarg.h" 3 4
    typedef __builtin_va_list __gnuc_va_list;
    # 37 "/usr/include/stdio.h" 2 3 4
    
    # 1 "/usr/include/x86_64-linux-gnu/bits/types.h" 1 3 4
    # 27 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
    # 28 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4
    # 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
    # 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4
    # 29 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
    
    
    typedef unsigned char __u_char;
    typedef unsigned short int __u_short;
    typedef unsigned int __u_int;
    typedef unsigned long int __u_long;
    
    
    typedef signed char __int8_t;
    typedef unsigned char __uint8_t;
    typedef signed short int __int16_t;
    typedef unsigned short int __uint16_t;
    typedef signed int __int32_t;
    typedef unsigned int __uint32_t;
    
    typedef signed long int __int64_t;
    typedef unsigned long int __uint64_t;
    
    
    
    
    
    
    typedef __int8_t __int_least8_t;
    typedef __uint8_t __uint_least8_t;
    typedef __int16_t __int_least16_t;
    typedef __uint16_t __uint_least16_t;
    typedef __int32_t __int_least32_t;
    typedef __uint32_t __uint_least32_t;
    typedef __int64_t __int_least64_t;
    typedef __uint64_t __uint_least64_t;
    
    
    
    typedef long int __quad_t;
    typedef unsigned long int __u_quad_t;
    
    
    
    
    
    
    
    typedef long int __intmax_t;
    typedef unsigned long int __uintmax_t;
    # 141 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/typesizes.h" 1 3 4
    # 142 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/time64.h" 1 3 4
    # 143 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
    
    
    typedef unsigned long int __dev_t;
    typedef unsigned int __uid_t;
    typedef unsigned int __gid_t;
    typedef unsigned long int __ino_t;
    typedef unsigned long int __ino64_t;
    typedef unsigned int __mode_t;
    typedef unsigned long int __nlink_t;
    typedef long int __off_t;
    typedef long int __off64_t;
    typedef int __pid_t;
    typedef struct { int __val[2]; } __fsid_t;
    typedef long int __clock_t;
    typedef unsigned long int __rlim_t;
    typedef unsigned long int __rlim64_t;
    typedef unsigned int __id_t;
    typedef long int __time_t;
    typedef unsigned int __useconds_t;
    typedef long int __suseconds_t;
    typedef long int __suseconds64_t;
    
    typedef int __daddr_t;
    typedef int __key_t;
    
    
    typedef int __clockid_t;
    
    
    typedef void * __timer_t;
    
    
    typedef long int __blksize_t;
    
    
    
    
    typedef long int __blkcnt_t;
    typedef long int __blkcnt64_t;
    
    
    typedef unsigned long int __fsblkcnt_t;
    typedef unsigned long int __fsblkcnt64_t;
    
    
    typedef unsigned long int __fsfilcnt_t;
    typedef unsigned long int __fsfilcnt64_t;
    
    
    typedef long int __fsword_t;
    
    typedef long int __ssize_t;
    
    
    typedef long int __syscall_slong_t;
    
    typedef unsigned long int __syscall_ulong_t;
    
    
    
    typedef __off64_t __loff_t;
    typedef char *__caddr_t;
    
    
    typedef long int __intptr_t;
    
    
    typedef unsigned int __socklen_t;
    
    
    
    
    typedef int __sig_atomic_t;
    # 39 "/usr/include/stdio.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h" 1 3 4
    
    
    
    
    # 1 "/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h" 1 3 4
    # 13 "/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h" 3 4
    typedef struct
    {
      int __count;
      union
      {
        unsigned int __wch;
        char __wchb[4];
      } __value;
    } __mbstate_t;
    # 6 "/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h" 2 3 4
    
    
    
    
    typedef struct _G_fpos_t
    {
      __off_t __pos;
      __mbstate_t __state;
    } __fpos_t;
    # 40 "/usr/include/stdio.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h" 1 3 4
    # 10 "/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h" 3 4
    typedef struct _G_fpos64_t
    {
      __off64_t __pos;
      __mbstate_t __state;
    } __fpos64_t;
    # 41 "/usr/include/stdio.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/types/__FILE.h" 1 3 4
    
    
    
    struct _IO_FILE;
    typedef struct _IO_FILE __FILE;
    # 42 "/usr/include/stdio.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/types/FILE.h" 1 3 4
    
    
    
    struct _IO_FILE;
    
    
    typedef struct _IO_FILE FILE;
    # 43 "/usr/include/stdio.h" 2 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h" 1 3 4
    # 35 "/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h" 3 4
    struct _IO_FILE;
    struct _IO_marker;
    struct _IO_codecvt;
    struct _IO_wide_data;
    
    
    
    
    typedef void _IO_lock_t;
    
    
    
    
    
    struct _IO_FILE
    {
      int _flags;
    
    
      char *_IO_read_ptr;
      char *_IO_read_end;
      char *_IO_read_base;
      char *_IO_write_base;
      char *_IO_write_ptr;
      char *_IO_write_end;
      char *_IO_buf_base;
      char *_IO_buf_end;
    
    
      char *_IO_save_base;
      char *_IO_backup_base;
      char *_IO_save_end;
    
      struct _IO_marker *_markers;
    
      struct _IO_FILE *_chain;
    
      int _fileno;
      int _flags2;
      __off_t _old_offset;
    
    
      unsigned short _cur_column;
      signed char _vtable_offset;
      char _shortbuf[1];
    
      _IO_lock_t *_lock;
    
    
    
    
    
    
    
      __off64_t _offset;
    
      struct _IO_codecvt *_codecvt;
      struct _IO_wide_data *_wide_data;
      struct _IO_FILE *_freeres_list;
      void *_freeres_buf;
      size_t __pad5;
      int _mode;
    
      char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
    };
    # 44 "/usr/include/stdio.h" 2 3 4
    # 52 "/usr/include/stdio.h" 3 4
    typedef __gnuc_va_list va_list;
    # 63 "/usr/include/stdio.h" 3 4
    typedef __off_t off_t;
    # 77 "/usr/include/stdio.h" 3 4
    typedef __ssize_t ssize_t;
    
    
    
    
    
    
    typedef __fpos_t fpos_t;
    # 133 "/usr/include/stdio.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/stdio_lim.h" 1 3 4
    # 134 "/usr/include/stdio.h" 2 3 4
    # 143 "/usr/include/stdio.h" 3 4
    extern FILE *stdin;
    extern FILE *stdout;
    extern FILE *stderr;
    
    
    
    
    
    
    extern int remove (const char *__filename) __attribute__ ((__nothrow__ , __leaf__));
    
    extern int rename (const char *__old, const char *__new) __attribute__ ((__nothrow__ , __leaf__));
    
    
    
    extern int renameat (int __oldfd, const char *__old, int __newfd,
           const char *__new) __attribute__ ((__nothrow__ , __leaf__));
    # 178 "/usr/include/stdio.h" 3 4
    extern int fclose (FILE *__stream);
    # 188 "/usr/include/stdio.h" 3 4
    extern FILE *tmpfile (void)
      __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ;
    # 205 "/usr/include/stdio.h" 3 4
    extern char *tmpnam (char[20]) __attribute__ ((__nothrow__ , __leaf__)) ;
    
    
    
    
    extern char *tmpnam_r (char __s[20]) __attribute__ ((__nothrow__ , __leaf__)) ;
    # 222 "/usr/include/stdio.h" 3 4
    extern char *tempnam (const char *__dir, const char *__pfx)
       __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (__builtin_free, 1)));
    
    
    
    
    
    
    extern int fflush (FILE *__stream);
    # 239 "/usr/include/stdio.h" 3 4
    extern int fflush_unlocked (FILE *__stream);
    # 258 "/usr/include/stdio.h" 3 4
    extern FILE *fopen (const char *__restrict __filename,
          const char *__restrict __modes)
      __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ;
    
    
    
    
    extern FILE *freopen (const char *__restrict __filename,
            const char *__restrict __modes,
            FILE *__restrict __stream) ;
    # 293 "/usr/include/stdio.h" 3 4
    extern FILE *fdopen (int __fd, const char *__modes) __attribute__ ((__nothrow__ , __leaf__))
      __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ;
    # 308 "/usr/include/stdio.h" 3 4
    extern FILE *fmemopen (void *__s, size_t __len, const char *__modes)
      __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ;
    
    
    
    
    extern FILE *open_memstream (char **__bufloc, size_t *__sizeloc) __attribute__ ((__nothrow__ , __leaf__))
      __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ;
    # 328 "/usr/include/stdio.h" 3 4
    extern void setbuf (FILE *__restrict __stream, char *__restrict __buf) __attribute__ ((__nothrow__ , __leaf__));
    
    
    
    extern int setvbuf (FILE *__restrict __stream, char *__restrict __buf,
          int __modes, size_t __n) __attribute__ ((__nothrow__ , __leaf__));
    
    
    
    
    extern void setbuffer (FILE *__restrict __stream, char *__restrict __buf,
             size_t __size) __attribute__ ((__nothrow__ , __leaf__));
    
    
    extern void setlinebuf (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    
    
    
    
    
    
    
    extern int fprintf (FILE *__restrict __stream,
          const char *__restrict __format, ...);
    
    
    
    
    extern int printf (const char *__restrict __format, ...);
    
    extern int sprintf (char *__restrict __s,
          const char *__restrict __format, ...) __attribute__ ((__nothrow__));
    
    
    
    
    
    extern int vfprintf (FILE *__restrict __s, const char *__restrict __format,
           __gnuc_va_list __arg);
    
    
    
    
    extern int vprintf (const char *__restrict __format, __gnuc_va_list __arg);
    
    extern int vsprintf (char *__restrict __s, const char *__restrict __format,
           __gnuc_va_list __arg) __attribute__ ((__nothrow__));
    
    
    
    extern int snprintf (char *__restrict __s, size_t __maxlen,
           const char *__restrict __format, ...)
         __attribute__ ((__nothrow__)) __attribute__ ((__format__ (__printf__, 3, 4)));
    
    extern int vsnprintf (char *__restrict __s, size_t __maxlen,
            const char *__restrict __format, __gnuc_va_list __arg)
         __attribute__ ((__nothrow__)) __attribute__ ((__format__ (__printf__, 3, 0)));
    # 403 "/usr/include/stdio.h" 3 4
    extern int vdprintf (int __fd, const char *__restrict __fmt,
           __gnuc_va_list __arg)
         __attribute__ ((__format__ (__printf__, 2, 0)));
    extern int dprintf (int __fd, const char *__restrict __fmt, ...)
         __attribute__ ((__format__ (__printf__, 2, 3)));
    
    
    
    
    
    
    
    extern int fscanf (FILE *__restrict __stream,
         const char *__restrict __format, ...) ;
    
    
    
    
    extern int scanf (const char *__restrict __format, ...) ;
    
    extern int sscanf (const char *__restrict __s,
         const char *__restrict __format, ...) __attribute__ ((__nothrow__ , __leaf__));
    
    
    
    
    
    # 1 "/usr/include/x86_64-linux-gnu/bits/floatn.h" 1 3 4
    # 119 "/usr/include/x86_64-linux-gnu/bits/floatn.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/floatn-common.h" 1 3 4
    # 24 "/usr/include/x86_64-linux-gnu/bits/floatn-common.h" 3 4
    # 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
    # 25 "/usr/include/x86_64-linux-gnu/bits/floatn-common.h" 2 3 4
    # 120 "/usr/include/x86_64-linux-gnu/bits/floatn.h" 2 3 4
    # 431 "/usr/include/stdio.h" 2 3 4
    
    
    
    extern int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...) __asm__ ("" "__isoc99_fscanf")
    
                                   ;
    extern int scanf (const char *__restrict __format, ...) __asm__ ("" "__isoc99_scanf")
                                  ;
    extern int sscanf (const char *__restrict __s, const char *__restrict __format, ...) __asm__ ("" "__isoc99_sscanf") __attribute__ ((__nothrow__ , __leaf__))
    
                          ;
    # 459 "/usr/include/stdio.h" 3 4
    extern int vfscanf (FILE *__restrict __s, const char *__restrict __format,
          __gnuc_va_list __arg)
         __attribute__ ((__format__ (__scanf__, 2, 0))) ;
    
    
    
    
    
    extern int vscanf (const char *__restrict __format, __gnuc_va_list __arg)
         __attribute__ ((__format__ (__scanf__, 1, 0))) ;
    
    
    extern int vsscanf (const char *__restrict __s,
          const char *__restrict __format, __gnuc_va_list __arg)
         __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__format__ (__scanf__, 2, 0)));
    
    
    
    
    
    extern int vfscanf (FILE *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vfscanf")
    
    
    
         __attribute__ ((__format__ (__scanf__, 2, 0))) ;
    extern int vscanf (const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vscanf")
    
         __attribute__ ((__format__ (__scanf__, 1, 0))) ;
    extern int vsscanf (const char *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vsscanf") __attribute__ ((__nothrow__ , __leaf__))
    
    
    
         __attribute__ ((__format__ (__scanf__, 2, 0)));
    # 513 "/usr/include/stdio.h" 3 4
    extern int fgetc (FILE *__stream);
    extern int getc (FILE *__stream);
    
    
    
    
    
    extern int getchar (void);
    
    
    
    
    
    
    extern int getc_unlocked (FILE *__stream);
    extern int getchar_unlocked (void);
    # 538 "/usr/include/stdio.h" 3 4
    extern int fgetc_unlocked (FILE *__stream);
    # 549 "/usr/include/stdio.h" 3 4
    extern int fputc (int __c, FILE *__stream);
    extern int putc (int __c, FILE *__stream);
    
    
    
    
    
    extern int putchar (int __c);
    # 565 "/usr/include/stdio.h" 3 4
    extern int fputc_unlocked (int __c, FILE *__stream);
    
    
    
    
    
    
    
    extern int putc_unlocked (int __c, FILE *__stream);
    extern int putchar_unlocked (int __c);
    
    
    
    
    
    
    extern int getw (FILE *__stream);
    
    
    extern int putw (int __w, FILE *__stream);
    
    
    
    
    
    
    
    extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
         __attribute__ ((__access__ (__write_only__, 1, 2)));
    # 632 "/usr/include/stdio.h" 3 4
    extern __ssize_t __getdelim (char **__restrict __lineptr,
                                 size_t *__restrict __n, int __delimiter,
                                 FILE *__restrict __stream) ;
    extern __ssize_t getdelim (char **__restrict __lineptr,
                               size_t *__restrict __n, int __delimiter,
                               FILE *__restrict __stream) ;
    
    
    
    
    
    
    
    extern __ssize_t getline (char **__restrict __lineptr,
                              size_t *__restrict __n,
                              FILE *__restrict __stream) ;
    
    
    
    
    
    
    
    extern int fputs (const char *__restrict __s, FILE *__restrict __stream);
    
    
    
    
    
    extern int puts (const char *__s);
    
    
    
    
    
    
    extern int ungetc (int __c, FILE *__stream);
    
    
    
    
    
    
    extern size_t fread (void *__restrict __ptr, size_t __size,
           size_t __n, FILE *__restrict __stream) ;
    
    
    
    
    extern size_t fwrite (const void *__restrict __ptr, size_t __size,
            size_t __n, FILE *__restrict __s);
    # 702 "/usr/include/stdio.h" 3 4
    extern size_t fread_unlocked (void *__restrict __ptr, size_t __size,
             size_t __n, FILE *__restrict __stream) ;
    extern size_t fwrite_unlocked (const void *__restrict __ptr, size_t __size,
              size_t __n, FILE *__restrict __stream);
    
    
    
    
    
    
    
    extern int fseek (FILE *__stream, long int __off, int __whence);
    
    
    
    
    extern long int ftell (FILE *__stream) ;
    
    
    
    
    extern void rewind (FILE *__stream);
    # 736 "/usr/include/stdio.h" 3 4
    extern int fseeko (FILE *__stream, __off_t __off, int __whence);
    
    
    
    
    extern __off_t ftello (FILE *__stream) ;
    # 760 "/usr/include/stdio.h" 3 4
    extern int fgetpos (FILE *__restrict __stream, fpos_t *__restrict __pos);
    
    
    
    
    extern int fsetpos (FILE *__stream, const fpos_t *__pos);
    # 786 "/usr/include/stdio.h" 3 4
    extern void clearerr (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    
    extern int feof (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    
    extern int ferror (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    
    
    
    extern void clearerr_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    extern int feof_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    extern int ferror_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    
    
    
    
    
    
    
    extern void perror (const char *__s);
    
    
    
    
    extern int fileno (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    
    
    
    
    extern int fileno_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    # 823 "/usr/include/stdio.h" 3 4
    extern int pclose (FILE *__stream);
    
    
    
    
    
    extern FILE *popen (const char *__command, const char *__modes)
      __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (pclose, 1))) ;
    
    
    
    
    
    
    extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__))
      __attribute__ ((__access__ (__write_only__, 1)));
    # 867 "/usr/include/stdio.h" 3 4
    extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    
    
    
    extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    
    
    extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    # 885 "/usr/include/stdio.h" 3 4
    extern int __uflow (FILE *);
    extern int __overflow (FILE *, int);
    # 902 "/usr/include/stdio.h" 3 4
    
    # 2 "main.c" 2
    
    # 2 "main.c"
    int main() {
       printf("Hello, World!");
       return 0;
    }
    

    Compilation

    enter image description here

    • The input file for this stage is *.i file.
    • A tool called compiler takes the output of the preprocessor and converts it to assembly language.
    • Assembly code, often referred to as assembly language or just assembly, is a low-level programming language that uses mnemonic symbols (e.g., MOV for "move," ADD for "add") to represent CPU operations (the so-called opcode), thus making the code human-readable.
    • This is exactly the same code you may have worked with when programming microcontrollers directly in assembly language (e.g., 8051 microcontroller).
    • The output file is *.s or *.asm file.
      gcc -S main.i -o main.s
    
    • -S: Stop after the stage of compilation proper;

    Strictly speaking, this is the most crucial part of the compilation process and cannot be done by no one but the gcc compiler. When we use the gcc to directly generate the executable file (without breaking down all the steps), what happens under the hood is: The gcc delegates all other compilation steps (preprocessing, assembling, and linking) to cpp, as, and ld, respectively, and only performs the compilation step. By doing so, we, C developers, can be unworried in calling such tools as it is already done by gcc. Therefore, we can work with a unique command interface (the gcc) to perform all the compilation process.

    the output is

        .file   "main.c"
        .text
        .section    .rodata
    .LC0:
        .string "Hello, World!"
        .text
        .globl  main
        .type   main, @function
    main:
    .LFB0:
        .cfi_startproc
        endbr64
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        leaq    .LC0(%rip), %rax
        movq    %rax, %rdi
        movl    $0, %eax
        call    printf@PLT
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
    .LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
        .section    .note.GNU-stack,"",@progbits
        .section    .note.gnu.property,"a"
        .align 8
        .long   1f - 0f
        .long   4f - 1f
        .long   5
    0:
        .string "GNU"
    1:
        .align 8
        .long   0xc0000002
        .long   3f - 2f
    2:
        .long   0x3
    3:
        .align 8
    4:
    

    Assembling

    enter image description here

    • The input file for this stage is *.asm or *.s file.
    • If you use gcc to assemble the file, you must name the extension to .s. Otherwise, the -c option of gcc will try to link the file instead of assembling it.
    • Each assembly instruction typically represents a single machine code instruction, which is a set of 0's and 1's. A part of this binary sequence is the opcode, while the remainder is usually memory addresses of variables or a numeric value. While Assembly is a programming language that is independent from the CPU architecture, the machine code it represents is specific to the target CPU. In other words, the machine code is highly platform-specific, meaning that it is tailored to a specific computer architecture and operating system. The machine instructions for one type of CPU will probably not run on a different CPU architecture without modification.
    • The tool that converts Assembly code into machine code is called assembler, and its task is called assembling.
    • In low-level microcontroller programming, the assembler converts the assembly code into a binary sequence (typically represented in hexadecimal) and embed it into the microcontroller. The micro is now ready to run.
    • On the other hand, in C/C++, the assembler converts the assembly code into a .o intermediate file, known as object file. This intermediate representation contains machine-level code, that is, non-human-readable instructions that represent the functions and variables defined in your code. The .o file is not the final executable code, though. That is, it contains machine-readable instructions but cannot be directly executed by the operating system. This is because some information necessary for a file to be executable by the OS is missing, such as the memory addresses of functions and variables, which is resolved during the linking phase.
    • The output file is *.o or *.obj file.
      gcc -c main.s -o main.o
    

    Alternatively, you can run

      as main.s -o main2.o
    

    where as the the GNU assembler. Again, you can check whether both approaches produce the same output:

      ❯ cmp main.o main2.o && echo "Files are equal" || echo "Files are not equal"
      Files are equal
    

    linking

    • The input file for this stage is *.o file.
    • The linker merges all the object code from multiple modules into a single one. If we are using a function from libraries, linker will link our code with that library function code.
    • The C Standard Libraries are typically bundled with the gcc compiler. When you install gcc or any other C compiler, it includes the necessary standard libraries required to compile and link C programs. These libraries are an integral part of the compiler distribution. Therefore, they don't exist as a separate shared library file on your system and you won't find it by looking for a .a (static library) file. The shared libraries (the .so files) are not linked at compile time, but only in the runtime.
    • For libraries outside the C standard library, you must to link it manually.
    • The output is non-readable.
      gcc main.o -o myprogram
    

    where myprogram is the executable file. Alternatively, you could directly use the ld command, but guess what? Although it is not the most crucial part, manually linking you object file to an executable file is COMPLICATED AS HELL. At this point you are literally twiddling bits to perfectly match with the hardware and OS specifications of your machine. You must understand stuffs such as computer organization and architecture, endianness, file types, dynamic linker/loader paths, BuildID, OS compatibility, magic numbers, stripped and non-stripped executable, etc... Thankfully, gcc already knows all these stuffs and wonderfully does this hard work for us by internally invoking ld with all the necessary options set. I suppose the ld options is out of the scope of this question. It is also important to state that, in practice, is highly not recommended to directly use ld. You should use gcc (or other compiler) instead.

    Finally

    ❯ ./myprogram
    Hello, World!