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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
List all Fortran tutorials.