Array Pitfalls

Address Calculation
double precision :: list(LIST_SIZE)
integer :: index
double precision :: tolerance
            

An array is a contiguous block of memory. Assuming the base address of list is 1000, the array would map into memory as follows:

Table 24.1. Memory Map of an Array

AddressContent
1000-1007list(1)
1008-1015list(2)
......
8992-8999list(1000)

The address of an array element with index i is the base-address + (i - lowest-subscript) * (number of memory cells used by 1 element).

E.g., the address of list(i) = 1000 + (i-1) * 8.

            address of list(1) = 1000 + (1-1) * 8 = 1000
            address of list(2) = 1000 + (2-1) * 8 = 1008
            ...
            address of list(1000) = 1000 + (1000-1) * 8 = 8992
            
Out-of-bounds Errors

Most compilers don't generate machine code that checks array subscripts. Validating the subscript on every array reference would slow down array access significantly.

The programmer must ensure that their code uses only valid array subscripts!

int     list[5], c;

for (c = 0; c <= 5; ++c)
{
    list[c] = 0
    printf("%d\n", c);
}
            
integer :: list(5), i

do i = 1, 6
    list(i) = 0
    print *, i
enddo
            

This code will overrun the end of the array 'list'.

Assume the base address of list is 4000.

Table 24.2. Memory Map of list and i

AddressContent
4000-4003list(1)
4004-4007list(2)
4008-4011list(3)
4012-4015list(4)
4016-4019list(5)
4020-4023i

When setting list(6), it computes the address 4000 + 4 * (6-1) = 4020, which is the address of the integer variable 'i'. It places a 4 byte integer 0 there, overwriting 'i', and causing the loop to start over.

This demonstrates the value of using named constants or variables to specify array boundaries!

integer :: list(LIST_MAX), i

do i = 1, LIST_MAX
    list(i) = 0
    print *, i
enddo
            

It's easy to type 6 when you meant 5, but not so easy to type LIST_MAX+1 when you meant LIST_MAX.

Pinpointing Crashes with a Debugger

Out-of-bounds array subscripts can cause a variety of problems.

  • They may cause data corruption that reveals itself immediately as shown above.
  • They may corrupt other data in a way that doesn't cause any problems, or causes problems at a later stage of the program.
  • They may cause different types of corruption depending on the compiler, the operating system, and the types of optimizations you compiled with. (You will often hear inexperienced programmers claim that the optimizer broke their program. This is possible in theory, but the vast majority of the time, the optimizer simply exposed a bug in their code by changing the type of corruption it caused.)
  • Finally, if you're lucky, a runaway subscript may cause your program to crash. In Unix, this is usually associated with a segmentation fault or a bus error. Both of these errors indicate that the program attempted an illegal memory access.

    A segmentation fault means an attempt to access memory in a way that's not allowed (e.g. a memory address not allocated for data), whereas a bus error indicates an attempt to access memory that does not physically exist, an attempt to write to a ROM, etc.

    These errors often cause the program to dump a core, which is a snapshot of the program code and data as it appeared in memory at the moment of the crash. The core file can be used by a debugger to pinpoint exactly which statement in the program was executing when the crash occurred. For most Unix compilers, compiling with the -g flag causes more useful information for debuggers to be placed in the executable file. Increasing the optimization level (-O, -O2, -O3) reduces the debugger's ability to pinpoint problems. For more information, consult the documentation on your compiler and debugger.