ZetCode

Compiling and debugging Fortran

last modified November 14, 2026

A compiler is a tool for finding mistakes, and gfortran can do much more of that than a plain gfortran prog.f90 suggests. It can report suspicious declarations before the program runs, insert checks that turn a silent out-of-bounds write into a message that names the index, trap a floating-point exception instead of letting it become an infinity, and produce debug information for a symbolic debugger. The separate compilation of modules and the make program that drives it complete the working setup.

This article starts from a program full of warnings and cleans it up, catches an out-of-bounds write that the unchecked build hides, makes a division by zero stop in the debugger instead of printing Inf, walks through a loop with gdb, builds a module and its user separately to see the .mod file, writes a Makefile for the same project, and reads the vectorisation report that -O2 produces.

Warnings before anything runs

-Wall turns on the warnings that are almost always mistakes, and -Wextra adds the arguable ones. The most common are an unused variable, an unused procedure and an unused dummy argument; none of them stops the program, and each of them is a sign of a half-finished edit. The program below has a variable that is never read, a subroutine that is never called and a dummy argument that is never used.

warn.f90
program warn
    implicit none
    integer :: i, total
    real :: slack

    total = 0
    do i = 1, 10
        total = total + i
    end do
    print '(a, i0)', 'total: ', total
contains
    subroutine report(value, label)
        integer, intent(in) :: value
        character(len=*), intent(in) :: label

        print '(a, a, i0)', label, ': ', value
    end subroutine report
end program warn

The compiler names the line, the name and the switch that produced each diagnostic, which is enough to find the code without an editor plugin. Warnings are not errors, so the executable is still produced and still works; that is why they are easy to ignore and why the habit of compiling with the flags from the first day matters.

$ gfortran -std=f2018 -Wall -Wextra warn.f90 -o warn
warn.f90:4:17:

    4 |     real :: slack
      |                 1
Warning: Unused variable ‘slack’ declared at (1) [-Wunused-variable]
warn.f90:12:21:

   12 |     subroutine report(value, label)
      |                     ^
Warning: ‘report’ defined but not used [-Wunused-function]

The same program with the three names used compiles without a word. The subroutine is called once, the dummy arguments are both read, and the real variable is printed, so every declaration has a purpose and the compiler has nothing to report.

clean.f90
program clean
    implicit none
    integer :: i, total
    real :: slack

    slack = 0.5
    total = 0
    do i = 1, 10
        total = total + i
    end do
    call report(total, 'total')
    print '(a, f0.2)', 'the slack: ', slack
contains
    subroutine report(value, label)
        integer, intent(in) :: value
        character(len=*), intent(in) :: label

        print '(a, a, i0)', label, ': ', value
    end subroutine report
end program clean

The output is the total of the first ten integers and the value of the real, printed with two decimals. Nothing about the program changed except that its parts are connected, and the compiler now trusts it enough to stay silent.

$ gfortran -std=f2018 -Wall -Wextra -fcheck=all clean.f90 -o clean
$ ./clean
total: 55
the slack: .50

Runtime checks

-fcheck=all inserts code that verifies array subscripts, pointer associations, substring bounds, memory allocation and the rules of the do loop at run time. A violation prints a message naming the index, the array and its bound, and stops the program with status 2. Without the flag the same write lands in whatever memory follows the array, and the program continues with a corrupted state that may not show up until much later. The checks know only about Fortran objects, so memory that comes from C or from a library is better watched with -fsanitize=address, which installs a guarded allocator and reports the address of the bad access.

bounds.f90
program bounds
    implicit none
    integer :: n, a(4), i, total

    n = 4
    total = 0
    do i = 1, n + 1
        a(i) = i * i
        total = total + a(i)
    end do
    print '(a, i0)', 'the total: ', total
end program bounds

The loop runs from one to n + 1 over an array of four elements, so the fifth assignment writes past the end. The checked build stops at that line and names the index 5 and the upper bound 4; the unchecked build prints 55 and exits with success, because the value it wrote just outside the array happened to be added to the total. The subtraction of the two totals would tell an attentive reader that something is wrong, but only the check states it.

$ gfortran -std=f2018 -Wall -Wextra bounds.f90 -o bounds_plain
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fno-backtrace bounds.f90 -o bounds_checked
$ ./bounds_plain; echo "exit: $?"
the total: 55
exit: 0
$ ./bounds_checked; echo "exit: $?"
At line 8 of file bounds.f90
Fortran runtime error: Index '5' of dimension 1 of array 'a' above upper bound of 4
exit: 2

Floating-point traps

By default an invalid floating-point operation produces a value from the IEEE rules: a division by zero gives an infinity, zero divided by zero gives a NaN, and the program carries on. -ffpe-trap=zero,invalid,overflow asks the processor to raise a signal on those operations instead, so the debugger stops at the instruction that caused them. The program below divides by zero deliberately and inspects the result.

fpe.f90
program fpe
    use ieee_arithmetic, only: ieee_is_finite, ieee_value, ieee_quiet_nan
    implicit none
    real :: x, y, z

    x = 1.0
    y = 0.0
    z = x / y
    print '(a, f0.1)', 'one divided by zero: ', z
    print '(a, l1)', 'the result is finite: ', ieee_is_finite(z)
    print '(a, g0)', 'a quiet nan prints as ', ieee_value(x, ieee_quiet_nan)
end program fpe

Without the trap the program prints Inf, reports that the value is not finite and prints a NaN, all with exit status zero. With the trap the same statement raises SIGFPE, the shell reports the signal, and the exit status is 136, which is 128 plus the signal number 8. The -fno-backtrace flag keeps the terminal free of addresses; without it gfortran prints the call stack, which is useful when the failing statement is not obvious.

$ gfortran -std=f2018 -Wall -Wextra -fcheck=all fpe.f90 -o fpe
$ ./fpe; echo "exit: $?"
one divided by zero: Inf
the result is finite: F
a quiet nan prints as NaN
exit: 0
$ gfortran -std=f2018 -Wall -Wextra -ffpe-trap=zero,overflow -fno-backtrace fpe.f90 -o fpetrap
$ ./fpetrap; echo "exit: $?"
bash: line 1: 25550 Floating point exception(core dumped) ./fpetrap
exit: 136

A session in gdb

-g puts the source line numbers and the variable names into the executable, and gdb uses them to stop at a line and print a value. Compiling with -no-pie fixes the load address of the program, which makes the addresses in the session reproducible; on a default build they change with every run. The program is small on purpose, so that the session can print every variable.

d1.f90
program d1
    implicit none
    integer, parameter :: n = 5
    integer :: i, a(n), total

    total = 0
    do i = 1, n
        a(i) = i * i
        total = total + a(i)
    end do
    print '(a, i0)', 'the total: ', total
end program d1

The breakpoint is placed on the addition, and the first stop finds the first iteration: i is 1, the element just computed is 1, and the total is still 0. The next command executes the statement and stops on the following line, after which the total is 1. The commands are passed with -ex in batch mode so that the transcript is a repeatable recording; interactively the same words are typed at the (gdb) prompt, and run, break, print, next, step, continue and backtrace cover most sessions.

$ gfortran -std=f2018 -g -O0 -no-pie -fcheck=all d1.f90 -o d1
$ gdb -batch -ex 'break d1.f90:9' -ex run -ex 'print i' -ex 'print a(1)' -ex 'print total' -ex next -ex 'print total' ./d1
Breakpoint 1 at 0x40124f: file d1.f90, line 9.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, d1 () at d1.f90:9
9	        total = total + a(i)
$1 = 1
$2 = 1
$3 = 0
7	    do i = 1, n
$4 = 1

Separate compilation and .mod files

A module is compiled on its own, and the compiler writes a .mod file that contains the interface of everything the module makes public. A program that uses the module reads that file at compile time, so the module must be compiled first; the object files can then be linked in any order. The example is a small geometry module and a program that prints a table of areas.

geometry.f90
module geometry
    use iso_fortran_env, only: real64
    implicit none
    private
    public :: circle_area, circumference

    real(real64), parameter :: pi = 3.141592653589793_real64
contains
    pure real(real64) function circle_area(radius) result(area)
        real(real64), intent(in) :: radius

        area = pi * radius * radius
    end function circle_area

    pure real(real64) function circumference(radius) result(length)
        real(real64), intent(in) :: radius

        length = 2.0_real64 * pi * radius
    end function circumference
end module geometry

The module keeps its constant private and exports two pure functions, which is the usual shape of a small library. Its interface reaches the user through geometry.mod, a binary file that no other compiler can read and that has to be regenerated whenever the module changes.

use_geometry.f90
program use_geometry
    use iso_fortran_env, only: real64
    use geometry
    implicit none
    real(real64), parameter :: radii(3) = [1.0_real64, 2.5_real64, 10.0_real64]
    integer :: i

    print '(a)', ' radius      area   circumference'
    do i = 1, size(radii)
        print '(f7.2, f10.3, f16.3)', radii(i), circle_area(radii(i)), &
            circumference(radii(i))
    end do
end program use_geometry

Compiling the program before the module fails with "Cannot open module file", which is the most common error in a project built by hand. The transcript compiles the module first, lists the .mod file, and then builds and runs the program. The -J option sends the module files to a directory of their choice, which keeps the source directory clean in a larger project.

$ rm -f geometry.mod geometry.o use_geometry.o areas
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -c use_geometry.f90 -o use_geometry.o
use_geometry.f90:3:9:

    3 |     use geometry
      |         1
Fatal Error: Cannot open module file ‘geometry.mod’ for reading at (1): No such file or directory
compilation terminated.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -c geometry.f90 -o geometry.o
$ ls geometry.mod
geometry.mod
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all use_geometry.f90 geometry.o -o areas
$ ./areas
 radius      area   circumference
   1.00     3.142           6.283
   2.50    19.635          15.708
  10.00   314.159          62.832

A Makefile

Once a project has more than two files, the order of the compilations and the list of objects become a program of their own. make reads a file of rules, each with a target, its prerequisites and the command that builds it, and rebuilds only what is out of date. The rule for the program depends on the two object files, and the rule for the user of the module also depends on geometry.mod, which is how make learns the order.

Makefile
FC = gfortran
FFLAGS = -std=f2018 -Wall -Wextra -fcheck=all

areas: use_geometry.o geometry.o
	$(FC) $(FFLAGS) use_geometry.o geometry.o -o areas

use_geometry.o: use_geometry.f90 geometry.mod
	$(FC) $(FFLAGS) -c use_geometry.f90 -o use_geometry.o

geometry.o: geometry.f90
	$(FC) $(FFLAGS) -c geometry.f90 -o geometry.o

geometry.mod: geometry.o

clean:
	rm -f *.o *.mod areas

.PHONY: clean

The variables at the top keep the compiler and the flags in one place, and the commands are indented with a tab, not spaces, which is the one detail that catches everyone. clean removes the products so that the build can start from nothing, and .PHONY tells make that it is not a file. The transcript cleans, builds everything with the commands printed, and runs make a second time, which reports that the program is up to date.

$ make clean
rm -f *.o *.mod areas
$ make
gfortran -std=f2018 -Wall -Wextra -fcheck=all -c geometry.f90 -o geometry.o
gfortran -std=f2018 -Wall -Wextra -fcheck=all -c use_geometry.f90 -o use_geometry.o
gfortran -std=f2018 -Wall -Wextra -fcheck=all use_geometry.o geometry.o -o areas
$ make
make: 'areas' is up to date.

Optimisation and its report

-O0 is the default and compiles for debugging; -O1, -O2 and -O3 enable increasing amounts of optimisation, and -O2 is the usual choice for a program that is ready to be measured. Among other things it vectorises loops, that is, it makes one instruction operate on several array elements at once. The flag -fopt-info-vec-optimized writes a report of every loop that was vectorised, and -fopt-info-vec-missed reports the ones that were not and why.

vector.f90
program vector
    use iso_fortran_env, only: real64
    implicit none
    integer, parameter :: n = 1000000
    real(real64) :: a(n), b(n)
    integer :: i

    do i = 1, n
        a(i) = real(i, real64)
    end do
    do i = 1, n
        b(i) = a(i) * 2.0_real64 + 1.0_real64
    end do
    print '(a, f0.1)', 'the mean of b: ', sum(b) / real(n, real64)
end program vector

The report names the three loops and the width of the vectors, and the program prints the mean of the second array, which is exactly n + 2 for this input. Vectorisation changes the order of the additions, so a sum computed in a vectorised loop can differ from the serial one in the last digits, the same effect that the parallel reductions showed. -fcheck=all works with -O2 as well, at the cost of some speed, and the two are best combined while a bug is being hunted.

$ gfortran -O2 -std=f2018 -Wall -Wextra -fopt-info-vec-optimized=vec.txt vector.f90 -o vector
$ cat vec.txt
vector.f90:14:66: optimized: loop vectorized using 16 byte vectors
vector.f90:11:10: optimized: loop vectorized using 16 byte vectors
vector.f90:8:10: optimized: loop vectorized using 16 byte vectors
vector.f90:14:66: optimized: basic block part vectorized using 16 byte vectors
vector.f90:14:66: optimized: loop vectorized using 16 byte vectors
vector.f90:11:10: optimized: loop vectorized using 16 byte vectors
vector.f90:8:10: optimized: loop vectorized using 16 byte vectors
vector.f90:14:66: optimized: basic block part vectorized using 16 byte vectors
vector.f90:14:66: optimized: loop vectorized using 16 byte vectors
vector.f90:11:10: optimized: loop vectorized using 16 byte vectors
vector.f90:8:10: optimized: loop vectorized using 16 byte vectors
vector.f90:14:66: optimized: basic block part vectorized using 16 byte vectors
$ ./vector
the mean of b: 1000002.0

Next steps

The unit testing article turns the checks of this one into a test suite, and the style guide collects the habits that keep a program easy to debug. The modules article explains the .mod file in more detail, the error handling article covers the status codes that a stopped program returns, and the FORTRAN 77 article shows why legacy code needs the -std=legacy flag in the first place.

Source

GNU Fortran manual: Invoking GNU Fortran

GNU Fortran manual: Debugging Options

GDB documentation

In this article we have compiled with -Wall and -Wextra and removed the warnings they found, used -fcheck=all to catch a write past the end of an array and compared it with the silent unchecked run, trapped a division by zero with -ffpe-trap, stopped a loop in gdb and printed its variables, built a module and its user separately to see the .mod file and the order it imposes, wrote a Makefile with dependencies, and read the vectorisation report of -O2. The previous article, FORTRAN 77 and legacy code, is the code these flags are usually pointed at.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.