C on Fedora — install gedit, compile with gcc and understand its errors (starting with the “command not found” of my first day in IC2)

Installing gedit in Fedora and compiling with gcc was the first thing I had to sort out in IC2. When they told us we’d program in C using gedit, the first thing I did was open Fedora’s terminal and type gedit. The answer was instant: command not found. Nobody had warned me it had to be installed.

In this guide I explain how to install gedit in Fedora, how to open it with & so it doesn’t block the terminal, and how to compile and run your first C program with gcc. But I don’t stop there: we also look at what gcc does inside (with the real figures of a compilation), how to read its error messages, how to compile a program made of several files and which options you should always use. If you don’t have Fedora yet, first follow the Fedora on VirtualBox and shared folder guides.

What you need before you start

  • Fedora running (in VirtualBox or installed for real) and a terminal open.
  • Internet in Fedora and your user’s password (for sudo).
  • Knowing your way around the terminal a little (cd, ls, mkdir). If not, have a look at the basic Linux commands first.

Every terminal session in this guide is real: I ran them once on a Linux with gcc 14 as a normal user called sergio. Fedora 44 ships gcc 16, which gives the same errors and warnings in the same format. The only thing I couldn’t run is gedit, because it’s a graphical window: for that you have my Fedora screenshots.

What is gcc and why do you need it?

gcc (GNU Compiler Collection) is Linux’s C compiler: it translates your .c into a program the processor can run. Fedora Workstation doesn’t always ship it. Check:

gcc --version

If the first line looks like gcc (GCC) 16.2.1 … (Fedora 44’s version in October 2026), you have it. If you get command not found, install it:

sudo dnf install -y gcc

If you did the shared folder guide through the CD road, gcc is already installed: step B asked for it.

Step 1 — Install gedit in Fedora

For a few versions now, the text editor that comes with Fedora Workstation is GNOME’s Text Editor, not gedit. That’s why gedit doesn’t exist until you install it. Open the terminal and run:

sudo dnf install -y gedit
Fedora terminal running sudo dnf install gedit with the list of packages to install

dnf is Fedora’s package manager: the equivalent of an app store, but from the terminal. It looks for gedit in Fedora’s repositories (on Fedora 44, version 50), downloads it with everything it needs and installs it. The -y answers “yes” to the confirmation question. It’ll take a few minutes depending on your connection. When it finishes, check it’s there:

gedit --version

In Fedora’s terminal, if you type a command that doesn’t exist, it sometimes offers to install the package that provides it (Install package 'gedit' to provide command 'gedit'? [N/y]). Answering y does the same as the dnf install.

Step 2 — Open gedit without blocking the terminal

Here’s the trick nobody tells you. If you open gedit by typing just:

gedit

the terminal gets blocked: you can’t type more commands while gedit is open. You’d have to open another terminal to compile, or close gedit every time. (You can also “free” it with Ctrl + Z followed by bg, but that’s the long way round.)

Fedora terminal blocked after opening gedit without the & symbol

The fix is the & operator, which runs the program in the background:

gedit &
Fedora terminal showing gedit's job number and PID after using & and the free prompt

With the & at the end, gedit opens and the terminal stays free. You’ll see something like [1] 4321: the [1] is the job number in that terminal and the other number is the PID (Process ID), the identifier of the gedit process across the whole system. Your PID will be different.

Foreground versus backgroundTop: gedit in the foreground blocks the terminal until gedit is closed. Bottom: with &, the terminal shows the job number and PID and stays free to compile and run while the gedit window stays open.gedit versus gedit &time →$ gedit hello.cterminal blockedgedit windowyou close gedit→ now you cangcc…$ gedit hello.c &[1] 4321gcc hello.c -o hello./hello…and anything elsegedit window open at the same time (background)[1] is the job number and 4321 the PID: yours will differ
With & both things run at once: you edit in gedit and compile in the terminal.

What is a background process?

In Linux, every program you run is a process. Processes normally run in the foreground: they hold the terminal until they finish. With & you send them to the background: they keep running, but the terminal is free.

gedit hello.c &    # opens gedit with a specific file, in the background
jobs               # lists this terminal's background jobs
fg                 # brings the last one to the foreground (Ctrl + Z stops it again)

In IC2 this is really handy: gedit with your code in one window and the terminal free next to it to compile and run without closing anything. (You’ll study processes in depth later in processes in C.)

Closing the terminal with gedit open in the background and losing what you hadn’t saved. The gedit process depends on that terminal: save with Ctrl + S before closing it.

Step 3 — Your first C file

Create a folder for your C labs and go into it:

mkdir -p ~/IC2/Labs/P1_introduction
cd ~/IC2/Labs/P1_introduction

The -p also creates the folders in between (IC2 and Labs) if they don’t exist, and ~ is your home folder. Now open gedit with your first file:

gedit hello.c &
gedit window with the C source file open next to the Fedora terminal

Type this code in gedit and save with Ctrl + S:

#include <stdio.h>

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

It’s the simplest possible C program. Before compiling it, let’s understand what each line does.

Understanding every line

  • #include <stdio.h>: tells the preprocessor to paste here the contents of stdio.h, the header file of the standard input/output library. Without this line gcc doesn’t know what printf is. It’s the closest thing to Python’s import.
  • int main(): the main function. Every C program has a main function: it’s the entry point, the first thing the system runs. The int says it returns an integer to the system.
  • printf("Hello world\n");: prints the text in the terminal. The \n is the newline: without it, the terminal’s next prompt would appear glued to the text.
  • return 0;: returns 0 to the system, which means “everything went fine”. A value other than 0 signals an error.

Step 4 — The C compilation model

Before compiling, understand what happens inside. In Python you run the .py directly. In C, the code goes through three phases before becoming a program:

gcc compilation phaseshello.c (6 lines) goes through the preprocessor (gcc -E) giving hello.i with 819 lines; the compiler (gcc -c) turns it into hello.o, machine code; the linker adds printf from the C library and produces the 15960-byte hello executable.What gcc hello.c -o hello does insidehello.c6 lineshello.i819 lineshello.omachine codehello15960 bytesPreprocessorgcc -Epastes stdio.hCompilergcc -cC → machine codeLinkergcc hello.o+ printf from libcgcc hello.c -o hello does the three phases in one go
The figures come from the real run: your Fedora will give similar ones (it depends on the C library version).
  1. Preprocessor. Handles the lines that start with #: pastes in the .h contents, replaces the #defines and strips the comments. Your #include <stdio.h> is expanded here.
  2. Compiler. Translates the preprocessed C into machine code and produces an object file .o: binary code, but not runnable yet, because it lacks the code for printf.
  3. Linker. Joins your .o with the libraries it needs (the C library, where printf lives) and produces the final executable.

When you type gcc hello.c -o hello, gcc does the three phases in one go. But you can see them separately. This is the real session:

$ gcc -E hello.c -o hello.i
$ wc -l hello.c hello.i
    6 hello.c
  819 hello.i
  825 total
$ tail -n 6 hello.i

# 3 "hello.c"
int main() {
    printf("Hello world\n");
    return 0;
}
$ gcc -c hello.c
$ ls
hello.c
hello.i
hello.o
$ gcc hello.o -o hello
$ ./hello
Hello world

The figures tell the story:

  • gcc -E stops after the preprocessor. Your hello.c has 6 lines; hello.i, 819. The other 813 are the contents of stdio.h and of the files it includes in turn: declarations of printf, scanf, FILE… (On your Fedora you’ll get a similar but different number: it depends on the C library version.)
  • tail -n 6 hello.i shows the end of the file: your main is there as is, below everything that was pasted. The # 3 "hello.c" line is a marker so gcc knows which line of your file each thing comes from when it reports errors.
  • gcc -c compiles without linking: hello.o appears.
  • gcc hello.o -o hello links and creates the executable, which works.

In practice you’ll always use the full command at once; this is for understanding where each type of error shows up, as we’ll see shortly.

Step 5 — Compile with gcc

Go back to the terminal (free thanks to the &) and compile. This is the full real session for steps 5 and 6:

$ ls -l
total 4
-rw-r--r-- 1 sergio sergio 78 Oct 26 19:57 hello.c
$ gcc hello.c -o hello
$ ls -l
total 20
-rwxr-xr-x 1 sergio sergio 15960 Oct 26 19:57 hello
-rw-r--r-- 1 sergio sergio    78 Oct 26 19:57 hello.c
$ ./hello
Hello world
$ hello
bash: hello: command not found
$ echo $?
127

What does each part of gcc hello.c -o hello mean?

PartWhat it is
gccThe C compiler
hello.cThe source file you want to compile
-o helloThe name of the executable it produces (output). Without -o, it’s called a.out

If gcc prints nothing, the compilation went well: just like javac, silence is good news. Look at the two ls -l:

  • Before, only hello.c was there, with -rw-r--r-- permissions: it can be read and written, but not executed.
  • Afterwards hello appears, with -rwxr-xr-x: that x is the execute permission gcc gives the executable. It weighs 15960 bytes, although your code is only 78: the rest is the structure of a Linux executable and the start-up code the linker adds.

The dates and the user (sergio) will be yours.

Fedora terminal with ls -l showing the C source and the executable with execute permission

Step 6 — Run the program

In the session above, ./hello prints Hello world. The ./ is mandatory: it tells the terminal the executable is in the current folder (.). Without it, the terminal looks for the program in the system folders (the PATH ones, like /usr/bin), doesn’t find it and answers bash: hello: command not found. And echo $?, which shows the exit code of the last command, gives 127: the code bash uses for “I can’t find that command”.

Fedora terminal running the compiled program and showing its greeting

The complete IC2 workflow

This is the cycle you’ll repeat thousands of times in IC2:

cd ~/IC2/Labs/P1_introduction    # 1. go to the lab folder
gedit program.c &                # 2. open the editor in the background
gcc -Wall program.c -o program   # 3. write, save (Ctrl + S) and compile
./program                        # 4. run it and look at the result

If gcc reports errors or the result isn’t what you expect: back to gedit, fix, save and repeat from 3. With the terminal’s ↑ arrow you get previous commands back without retyping them.

Type gcc -Wall and not just gcc. The -Wall option turns on the most useful warnings, and in the options section you’ll see a program that compiles “fine” and gives a wrong result that only -Wall gives away.

Understanding gcc error messages

All gcc messages have the same shape: file:line:column: type: description. The type can be error (no executable is produced) or warning (it is produced, but something smells). Below, gcc copies the offending line and marks the exact spot with ^. Each error comes from a different phase:

gcc errors by phaseIf the file does not exist, gcc stops before starting (No such file). A missing ; or printf without #include <stdio.h> is caught by the compiler. A function whose file was not passed to gcc is caught by the linker (undefined reference). Warnings do not stop compilation; -Wall turns them on.Each error comes from a different phaseBefore startinghla.c: No such fileor directorymissing or misspeltCompilerexpected ';'before 'return'a ; is missing at the endimplicit declarationof function 'printf'#include <stdio.h> is missingLinker (ld)undefined referenceto `greet'welcome.c is not in the commandwarning = it still compiles, but something smells · -Wall shows them all
Read the first line of the error: the phase tells you where to look.

Knowing the phase tells you where to look. Here are the errors you’ll see most when starting out, all from a real run.

Common mistakes when starting

Mistake 1 — Forgetting the &

gedit hello.c      # terminal blocked: you can't compile
gedit hello.c &    # correct: terminal free

There’s no message: the terminal simply won’t let you type. Close gedit (or press Ctrl + Z and type bg) and open it again with &.

Mistake 2 — Running without ./

You saw it in step 6: hello on its own gives bash: hello: command not found (code 127). It’s ./hello.

Mistake 3 — Forgetting return 0

The previous version of this guide said that forgetting return 0 in main gives a warning. It isn’t true, and I checked it while updating it. Since the C99 standard, if main reaches its end without return, it returns 0 automatically:

$ gcc -Wall no_return.c -o no_return
$ ./no_return
Hello world
$ echo $?
0

Not a single warning, even though I compiled with -Wall, and the exit code is 0. Where return does matter is in every other function that returns something. Look at this twice.c:

int twice(int x) {
    int r = x * 2;
}

It computes the double, stores it in r… and forgets to return it:

$ gcc -Wall -c twice.c
twice.c: In function 'twice':
twice.c:4:9: warning: unused variable 'r' [-Wunused-variable]
    4 |     int r = x * 2;
      |         ^
twice.c:5:1: warning: control reaches end of non-void function [-Wreturn-type]
    5 | }
      | ^

Two warnings: r is never used (unused variable), and the function reaches its end without returning anything although it promises an int (control reaches end of non-void function). Careful: it’s a warning, so gcc builds the program anyway, and whatever twice(4) returns is garbage: whatever was in the processor’s register at that moment. You might get 8 by chance and another number on another computer. The fix is writing return x * 2;.

Syntax error — missing semicolon

In C every statement ends with ;. It’s the most common syntax error at the start:

$ gcc -Wall no_semicolon.c -o no_semicolon
no_semicolon.c: In function 'main':
no_semicolon.c:4:28: error: expected ';' before 'return'
    4 |     printf("Hello world\n")
      |                            ^
      |                            ;
    5 |     return 0;
      |     ~~~~~~                  

Read the message calmly: no_semicolon.c:4:28 is line 4, column 28, just after the ) of printf. gcc even draws the missing ; under the ^. The funny thing is it says before ‘return’: gcc doesn’t notice the missing ; until it reaches the next word, which is on line 5. So when an error makes no sense on its line, look at the line before.

Using printf without #include <stdio.h>

$ gcc -Wall no_include.c -o no_include
no_include.c: In function 'main':
no_include.c:2:5: error: implicit declaration of function 'printf' [-Wimplicit-function-declaration]
    2 |     printf("Hello world\n");
      |     ^~~~~~
no_include.c:1:1: note: include '<stdio.h>' or provide a declaration of 'printf'
  +++ |+#include <stdio.h>
    1 | int main() {
no_include.c:2:5: warning: incompatible implicit declaration of built-in function 'printf' [-Wbuiltin-declaration-mismatch]
    2 |     printf("Hello world\n");
      |     ^~~~~~
no_include.c:2:5: note: include '<stdio.h>' or provide a declaration of 'printf'

Without the #include, the compiler reaches printf without knowing what it is (implicit declaration). In old gcc versions it was just a warning; since gcc 14 it’s an error and no program is built (Fedora 44 ships gcc 16, so it’ll happen to you just like here). The note gives you the literal fix: the +++ |+#include <stdio.h> line is gcc showing you what you’d have to add on line 1.

File not found

$ gcc helo.c -o hello
cc1: fatal error: helo.c: No such file or directory
compilation terminated.

No such file or directory: the file helo.c doesn’t exist. It hasn’t even started compiling (cc1 is the gcc program that does the real work). Check the name with ls and that you’re in the right folder with pwd.

Compiling multiple files

As soon as labs grow, code gets split: a .h with the declarations (which functions exist) and a .c with their code. For example:

void greet(const char *name);

That line is a promise: “there’s a function greet that takes a text and returns nothing”. The real code goes in welcome.c:

#include <stdio.h>
#include "welcome.h"

void greet(const char *name) {
    printf("Hello, %s. Welcome to IC2\n", name);
}

And the main program, which only includes the header and calls the function:

#include "welcome.h"

int main() {
    greet("Laura");
    greet("Pablo");
    return 0;
}

Notice that here the #include uses quotes ("welcome.h") and not < >: quotes mean “look in my folder first”; < >, “look in the system folders”. The real session:

$ ls
main.c
welcome.c
welcome.h
$ gcc main.c -o welcome
/usr/bin/ld: /tmp/cc8NcOWY.o: in function `main':
main.c:(.text+0x13): undefined reference to `greet'
/usr/bin/ld: main.c:(.text+0x22): undefined reference to `greet'
collect2: error: ld returned 1 exit status
$ gcc -Wall main.c welcome.c -o welcome
$ ./welcome
Hello, Laura. Welcome to IC2
Hello, Pablo. Welcome to IC2

The first attempt is the most typical linker error: undefined reference to 'greet'. main.c compiles fine, because welcome.h tells it greet exists; but at link time nobody has given gcc the code for greet, which is in welcome.c. (The /tmp/cc….o name is a gcc temporary file: yours will differ.) The fix is passing it every .c: gcc -Wall main.c welcome.c -o welcome. The .h files don’t go in the command: the preprocessor already pastes them in.

Useful gcc flags

Look at this program. It adds up three marks and says so if they reach 21:

#include <stdio.h>

int main() {
    int marks[3] = {7, 5, 8};
    int total = 0;
    int extra;
    for (int i = 0; i < 3; i++)
        total += marks[i];
    if (total = 21)
        printf("You reached 21\n");
    printf("Total: %d\n", total);
    return 0;
}

7 + 5 + 8 = 20, so it shouldn’t say anything. This is what happened:

$ gcc check.c -o check
$ ./check
You reached 21
Total: 21
$ gcc -Wall check.c -o check
check.c: In function 'main':
check.c:9:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
    9 |     if (total = 21)
      |         ^~~~~
check.c:6:9: warning: unused variable 'extra' [-Wunused-variable]
    6 |     int extra;
      |         ^~~~~

Without options gcc doesn’t complain, and the program says you reached 21… and that the total is 21. The bug is in if (total = 21): with a single = it doesn’t compare, it assigns 21 to total, and since 21 isn’t 0, the condition is true. With -Wall, gcc flags it (suggest parentheses around assignment used as truth value: “if you really mean to assign, put it in parentheses so it’s visible”) and, on the way, warns that extra is never used. The fix is if (total == 21).

The options worth knowing from the start:

OptionWhat it doesWhen?
-o nameName of the executableAlways
-WallTurns on the most useful warningsAlways
-WextraEven more warnings (unused parameters, odd comparisons…)When you want to be strict
-std=c11Uses the C11 standardIf your lecturer asks for it
-gAdds information for the debugger (gdb)When you’re going to debug
-lmLinks the maths library (sqrt, pow)If you use math.h; it goes at the end of the command
-E / -cStops after the preprocessor / after compilingTo understand the phases

Quick summary

sudo dnf install -y gedit gcc   # install the editor and the compiler
gedit file.c &                  # open the editor in the background
gcc -Wall file.c -o file        # compile with warnings
./file                          # run
gcc -Wall main.c other.c -o app # several files: every .c
MessagePhaseWhat it usually is
expected ';' before …CompilerA ; is missing on the previous line
implicit declaration of function …CompilerThe function’s #include is missing
undefined reference to …LinkerA .c is missing from the command (or -lm)
No such file or directoryBefore compilingThe file doesn’t exist or you’re not in its folder
command not foundThe terminalThe ./ is missing or the program isn’t installed

Three ideas to take away:

  1. Open gedit with &: editor and terminal at the same time.
  2. gcc goes through three phases (preprocessor, compiler and linker); the error message tells you which one failed.
  3. Always compile with -Wall: a program that compiles isn’t the same as a correct program.

With this you have the complete environment to program in C in IC2. In the next article we start with the language for real: types and variables in C, with printf and scanf to read and write data.

Keep goingAll Tools articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

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