3

I am developing a library in C that should work with C, C++, or Fortran code. One mechanism it uses is to trap writes to pages in the stack, heap, or data/bss segments. The "heap" in this case is a special heap that the library creates out of a mapped file. I found that my library was failing to trap a write to a variable in a Fortran application. The variable is declared as

double precision u(5,I,J,K)

Where I, J, and K are integer parameters (i.e., constants). The code then includes u in a common block called "fields."

When debugging under GDB, I found that the address of u did not fall into the range of any of the three data segments. (Hence the library failed to trap the write!) I then looked at the /proc//maps pseudo-file and found that the address of u falls into a range that the system annotates as "heap." But how did u get into this "heap"? The Fortran 77 code in this case does not use the nonstandard "allocate" keyword to allocate on the heap. Could anybody explain to me what variables Fortran 77 (under Ubuntu Linux x86-64) allocates on the "heap, " and how this "heap" gets created in the first place?

trincot
  • 317,000
  • 35
  • 244
  • 286
Amittai Aviram
  • 2,270
  • 3
  • 25
  • 32

1 Answers1

8

I played around with an array in a common block. What it looks like is that the .bss segment in Linux is really merged with the heap (or at least space is allocated using the same brk(2) mechanism).

Here is the relevant Fortran code:

double precision u(5,20,20,20)
common /a/ u

The GNU assembly directive that gfortran produces is:

.comm a_,320000,32

That declares a common symbol named a_ that is 320000 bytes large and should be aligned on a 32-byte boundary. When the linker sees this declaration and no other definitions of a_ it reserves space for it in .bss as is clearly visible from running objdump on the resulting binary:

 Sections:
 Idx Name          Size      VMA               LMA               File off  Algn
 ...
  22 .data         00000010  0000000000600b40  0000000000600b40  00000b40  2**3
                   CONTENTS, ALLOC, LOAD, DATA
  23 .bss          0004e220  0000000000600b60  0000000000600b60  00000b50  2**5
                   ALLOC
 ...

Here the .bss is 320000 (0x4e200) bytes plus some 32 bytes of additional data. It is only marked as allocatable and nothing more - no data is prepopulated from the file. You can also deduce that the 32 bytes of additional data is laid before a_ since u starts at VMA 0x600b80:

(gdb) info address u
Symbol "u" is static storage at address 0x600b80.
(gdb) info symbol &u
a_ in section .bss of /path/to/a.out

u is really just the symbol of a local variable in the Fortran main function, while a_ is the globally visible storage. That's why you can name arrays differently in different subroutines/functions but still access the same memory if you put them in the appropriate common block.

It looks like the awkward VMA of .bss is a result of the offset of the .data segment in the ELF file as the .bss is located immediately after the .data section in memory. The way .data segment is loaded in Linux is that it is mmap(2)-ed from the file with MAP_PRIVATE which gives the mapping a copy-on-write semantics:

00400000-00401000 r-xp 00000000 00:1d 25681168    /path/to/a.out
00600000-00601000 rw-p 00000000 00:1d 25681168    /path/to/a.out <-- .data
00601000-00670000 rw-p 00000000 00:00 0           [heap]

.bss starts in the same page as the .data mapping which makes sense since both hold read/write data and are expected to be written to and a bit of VM is conserved by not starting .bss on a separate page.

Everything after the .data segment is not backed up by a file mapping and thus falls in the dynamically adjustable space that is visible as [heap] in /proc/pid/maps. This space as well the heap itself is controlled by moving the end of the data segment, the so-called program break, with brk(2). The ELF loader in the kernel initially moves the program break far enough to reserve space for the .bss as can be seen by strace of the executable:

execve("./a.out", ["a.out"], [/* 230 vars */]) = 0
brk(0)                          = 0x64f000 <-- already moved past the .bss

We know that the .data segment starts at 00600000. The .bss starts at 00600B60. The common block is allocated at 0x600b80 and its size is 0x4e200 and so it ends at 0x64ed80, which rounded up to a page boundary gives 0x64f000. Here the real program heap would begin if no other dynamically linked libraries allocate space on their own.

Since the dynamic memory allocator malloc(3) uses the same brk(2) mechanism (or anonymous mmap(2) for large allocations or when the limit of the data segment size was exhausted) it really doesn't matter if the array was in .bss or was allocated with ALLOCATE(). The difference is that the .bss is filled with zeros initially while the content of memory allocated by malloc or ALLOCATE() is left as is.

Hristo Iliev
  • 72,659
  • 12
  • 135
  • 186