Electronics/Documentation

GCC Syntax

Last updated Dec 27, 2025Electronics

GCC Command Line Syntax

In this section, we’ll talk about a specific C compiler, GCC, and how to use it. If you are not already familiar with Bash/unix command line syntax, see here.

In general, GCC commands will appear as follows: gcc <input file(s)> -o <output file> <flags>. The flags section is where the commands get more interesting, but for a basic compilation we don’t actually need any.

For compiling C++ programs, the command is g++, but it follows the same syntax and flags as gcc.

Basic Program

For a program that consists of a single source file and no custom headers or libraries, the command is very simple: gcc source.c -o out.exe. This command will take a single source file source.c, compile it into a program and output it as out.exe. From there, it can be run as any other program.

Sometimes, you may have multiple source files, but the compilation is still very simple. You can actually specify multiple sources to include in one compile command, like follows: gcc source1.c source2.c ... -o out.exe. This will do practically the same as before, but it just combines all the sources you specify into a single translation unit, and proceeds with the compilation and linking as normal.

Custom Headers

Chances are, your project will have some custom headers that you want to be available in the include path. You can specify additional include directories with the -I flag, followed by the folder to add to the path.

For example, lets say our project has the structure as presented in the compilation process article, and we are compiling that program. We can add this to our compiler call like so: gcc src/file1.c src/file2.c src/file3.c -o out.exe -I ./include. The -I flag can be specified multiple times in a single call to add multiple folders.

Adding Libraries

Once again, chances are your program relies on some libraries other than purely the standard ones.

When adding static libraries, you will have to tell GCC that you want to link against them by adding them to the list of sources [1]. For example, if we want to link against lib/libcoolstuff.a, then we would simply add it to our command in the sources section: gcc src/file1.c src/file2.c src/file3.c libs/libcoolstuff.a -o out.exe -I ./include

Dynamic Libraries

If we are linking against a dynamic library/shared object[2], the process is a little different. GCC expects that libraries start with the characters lib, and end with .so, so when specifying libraries you actually leave these parts out of the name. For example, if there was a file lib/libfantasticstuff.so we wanted to link with, we would write: gcc src/file1.c src/file2.c src/file3.c -o out.exe -I ./include -lfantasticstuff. [3]

However, GCC also needs to know where to find the library file itself, since the libraries are likely not in the folder where we invoke the compiler. Since libfantasticstuff.so is in the lib folder, we can let GCC know about this with the -L flag: gcc src/file1.c src/file2.c src/file3.c -o out.exe -I ./include -L ./lib -lfantasticstuff.

Optionally, you can put a colon after the -l if you want to write the file path and name itself: gcc source1.c source2.c ... -o out.exe -I ./include -l:lib/libfantasticstuff.a.

Like with the -I flag, -L and -l can be specified multiple times for multiple libraries. We do not use shared libraries very much in LRI, so you can mostly ignore this process.

Pre-defined Macros

Very frequently (especially in a microcontroller/embedded context), you may have some features of your program you may not always want to be included in a build. For this, you will likely rely on using macros and #ifdef/#ifndef to conditionally compile sections of your code. This is great and all, but having to go into your code and add in or comment out some #defines every time you want change features is a real hassle.

Fortunately, you can pre-define macros on the command line itself. Lets say that we have some features in our program that are enabled if the macro FEATURE1 is defined. We can specify this on the command line, so every source file sees it automatically with the -D flag: gcc src/file1.c src/file2.c src/file3.c lib/libcoolstuff.a -o out.exe -I ./include -DFEATURE1.

We can optionally specify a value for the macro as well: gcc src/file1.c src/file2.c src/file3.c lib/libcoolstuff.a -o out.exe -I ./include -DFEATURE1=17.

GCC will automatically remove the D from the macro name for us.

Debugging Mode and Optimizations

By default, GCC will not include all the information in the output program for a debugging to be able to work correctly. If we want to be able to attach a debugger, we need to specify we want GCC to keep the debugging symbols around with the -g flag, In addition, we want to make sure GCC does not apply any code optiizations so everything in the binary matches exactly what we coded: gcc src/file1.c src/file2.c src/file3.c lib/libcoolstuff.a -o out.exe -I ./include -DFEATURE1 -g -O0.

In contrast, if we are making a release build, we want GCC to optimize our code. This includes remmoving any unncessary debugging symbols, that way the size of the final output is as small as possible. We can do this by changing out the last 2 flags: gcc src/file1.c src/file2.c src/file3.c lib/libcoolstuff.a -o out.exe -I ./include -DFEATURE1 -s -03, These flags indicate “strip” mode and optimization level 3.

In fact, there are 4 optimization levels: none, and levels 1, 2, and 3. These are indicated with -O0, -O1, -O2, and -O3. Each one introduces additional and more drastic types of optimization. For debug builds, you want no optimization, that way the final output matches your code exactly and it is easier to follow. For release builds, you want the output to be as fast and small as possible.

Warnings

GCC (as well as many other compilers) come with a set of patterns it will recognize as likely buggy. It indicates this to you with what are called warnings. They are not necessarily errors like a syntax mistake or a missing variable, but they are things you should try and fix. Warnings are things that the compiler thinks could go wrong at runtime. While it will still build your program, it will complain about it to let you know something is up, and is worth your attention.

You should ALWAYS fix warnings! Many people will simply ignore them because theyre not technically errors, but they are things that could cause your program to crash or produce wrong results. Letting warnings pile up is a fast track to accumulating technical debt, and warnings should be fixed as they come up.

Since warnings are so helpful, we would actually like GCC to look for every possible warning it can find. It would also be nice if it checked if our code complies exactly with the ISO standard for C/C++, that way our code is as portable as possible. In fact, it would be nice if GCC treated warnings as errors, that way we HAVE to fix them before being able to move on.

Fortunately, we can do this with a set of 4 additional flags: gcc src/file1.c src/file2.c src/file3.c lib/libcoolstuff.a -o out.exe -I ./include -DFEATURE1 -g -O0 -Wall -Wextra -Wpedantic -Werror. Each one of these flags applies more restrictions:

  • -Wall enables all the normal warnings
  • -Wextra enables even more warnings that aren’t necessarily as important, but should still be taken care of
  • -Wpedantic checks that our code complies with the ISO standards for C/C++. Its called pedantic mode because at this point we are really nitpicking
  • -Werror tells GCC to treat all warnings as if they were errors, and when encountering one it should stop compiling and notify the user

Language Standard

Over the years, there have been multiple standards published for versions of C and C++. These are versions checked and verified by ISO, or the International Organization for Standardization (yeah they got the order wrong), and are important milestone versions in each language. Each version contains different language features (things like keywords, or syntax), as well as different functions available in the standard library (for example, C++ 11 introduces standard mutlithreading). The versions available for each language are (the number corresponds to the year of release):

Versions
C c89 c90* c95 c99* c11* c17* c23*
C++ c++98* c++03 c++11* c++14* c++17* c++20* c++23* c++26

C++26 has not yet actually been released, but it is close and coming out with some exciting new features.

Knowing which language standard you are using is important, and also needs to be specified to the compiler. This is done with the -std=<ver> flag, where <ver> is one of the starred values in the table (the unstarred ones are technically C and C++ standard versions, but they are not important enough to worry about).

For example, if we decided we were using C11 for our project, we would indicate that like this: gcc src/file1.c src/file2.c src/file3.c lib/libcoolstuff.a -o out.exe -I ./include -DFEATURE1 -g -O0 -Wall -Wextra -Wpedantic -Werror -std=C11.

More Complex Compiling

We now know how to compile everything with a single line, but what if we want to follow a more complex and flexible compilation process to allow us to seperate our sources into their own translation units, and not have to recompile everything every time one file changes (like here)?

To do this, we will seperate out the calls to GCC. There will be one for each source file, where we let GCC know we are only compiling and not linking, and one final call that brings in all of our libraries and object files, and links them together (even though linking is technically done by a seperate program called ld, it can be invoked through GCC).

For each individual source, we will compile like this: gcc <source> -o <source.o> -c <other flags>. For example, for our project, we will need 3 lines:

  • gcc src/file1.c -o build/file1.o -I ./include -c -DFEATURE1 -g -O0 -Wall -Wextra -Wpedantic -Werror -std=C11
  • gcc src/file2.c -o build/file2.o -I ./include -c -DFEATURE1 -g -O0 -Wall -Wextra -Wpedantic -Werror -std=C11
  • gcc src/file3.c -o build/file3.o -I ./include -c -DFEATURE1 -g -O0 -Wall -Wextra -Wpedantic -Werror -std=C11

Each of these lines will generate an object file from the source file, and place it in a folder called build. Notice here we don’t specify anything about our libraries; they are not a part of the translation units for our individual sources[4]. This is because compiling only cares about translation, it does not deal with any kind of linking to functions or variables. Hence, we don’t need the libraries themselves for this step. We may need some headers that need to be added to the include path with -I for the libraries, but the .a files themselves come later.

However, all of the other flags we’ve talked about so far are required for the compile step, as these all affect things during the translation phase. Here we can also clearly see another advantage of seperating out our compilations: my computer has 16 cores, and so I could run each of these commands at the same time, thus reducing the amount of time I have to wait. This isn’t huge for a small program with 3 files, but when you have a few hundred sources you’re waiting on, this makes a huge difference.

Now that we have all our sources as object files, all that is left is to combine them together with any libraries to produce the final output: gcc build/file1.o build/file2.o build/file3.o lib/libcoolstuff.a -o out.exe. This step invokes the linker to perform the finishing touches on our program. [5]

Other Flags

There are a lot of other flags available in GCC that are not as important, but may still come up sometimes. For example, the -f flag can be used to enable or disable various platform features like floating point acceleration, and -m can be used to control platform specific optimizations.

Some flags that may be interesting to you could be -E and -S. By exchanging the -c flag in the compile step with one of these, you can view the output of the preprocessor or the raw assembly, respectively. These aren’t typically very helpful, but can be a very good learning experience if you want to more closely trace the path a piece of code follows through the compilation proocess.


  1. If you recall, static libraries like this are really just a bundle of precompiled object files, so it makes sense they are included in the sources section since thats basically what they are. ↩︎
  2. We did not talk about this library type yet, but it is basically a linked and ready to go library that can be shared amongst multiple programs. This is usually applicable for things like system libraries, like your graphics drivers. These are things managed by the OS, and you don’t really want to have to worry about compiling them yourself. The OS does a bunch of fancy things to ensure the library is made available to your program, rather than the program carrying it in the executable itself. ↩︎
  3. This is the process for shared objects on linux. For linking against DLLs on Windows, the process is a bit different but not worth explaining. ↩︎
  4. If we had any, this compilation step also would exclude any -L and -l flags for dynamic libraries ↩︎
  5. This last step is where we would include any dynamic libraries with -L and -l. ↩︎