Gfortran для макетов: что делает mcmodel = medium?

У меня есть код, который дает мне ошибки перемещения при компиляции, ниже приведен пример, который иллюстрирует проблему:

  program main
  common/baz/a,b,c
  real a,b,c
  b = 0.0
  call foo()
  print*, b
  end

  subroutine foo()
  common/baz/a,b,c
  real a,b,c

  integer, parameter :: nx = 450
  integer, parameter :: ny = 144
  integer, parameter :: nz = 144
  integer, parameter :: nf = 23*3
  real :: bar(nf,nx*ny*nz)

  !real, allocatable,dimension(:,:) :: bar
  !allocate(bar(nf,nx*ny*nz))

  bar = 1.0
  b = bar(12,32*138*42)

  return
  end

Компилируя это с помощью gfortran -O3 -g -o test test.f, я получаю следующую ошибку:

relocation truncated to fit: R_X86_64_PC32 against symbol `baz_' defined in COMMON section in /tmp/ccIkj6tt.o

Но он работает, если я использую gfortran -O3 -mcmodel=medium -g -o test test.f. Также обратите внимание, что он работает, если я делаю массив доступным и распределяю его внутри подпрограммы.

Мой вопрос: что именно делает -mcmodel=medium? У меня создалось впечатление, что две версии кода (одна с allocatable массивами и одна без) были более или менее эквивалентными...

Ответ 1

Так как bar довольно большой, компилятор генерирует статическое распределение вместо автоматического распределения в стеке. Статические массивы создаются с помощью директивы сборки .comm, которая создает распределение в так называемом разделе COMMON. Символы из этого раздела собираются, одинаковые имена объединяются (сводятся к одному запросу символа с размером, равным наибольшему запрошенному размеру), а затем то, что покой, сопоставляется с разделом BSS (неинициализированные данные) в большинстве исполняемых форматов. С помощью исполняемых файлов ELF секция .bss находится в сегменте данных непосредственно перед частью сегмента данных кучи (имеется еще одна часть кучи, управляемая отображениями анонимной памяти, которые не находятся в сегменте данных).

В модели памяти small для адресации символов x86_64 используются 32-разрядные инструкции адресации. Это делает код меньше и быстрее. Некоторая сборка при использовании модели памяти small:

movl    $bar.1535, %ebx    <---- Instruction length saving
...
movl    %eax, baz_+4(%rip) <---- Problem!!
...
.local  bar.1535
.comm   bar.1535,2575411200,32
...
.comm   baz_,12,16

Это использует 32-битную инструкцию перемещения (длиной 5 байт), чтобы поместить значение символа bar.1535 (это значение равно адресу места символа) в нижние 32 бита регистра RBX ( верхние 32 бита обнуляются). Сам символ bar.1535 выделяется с помощью директивы .comm. После этого выделяется память для блока baz COMMON. Поскольку bar.1535 очень большой, baz_ заканчивается более чем на 2 гигабайта с начала раздела .bss. Это создает проблему во второй инструкции movl, поскольку для обращения к переменной b, где должно быть перенесено значение EAX, необходимо использовать не 32-битное (подписанное) смещение от RIP. Это обнаружено только во время соединения. Сам ассемблер не знает соответствующего смещения, так как он не знает, что будет означать значение указателя инструкции (RIP) (оно зависит от абсолютного виртуального адреса, где загружается код, и это определяется компоновщиком), поэтому он просто переносит смещение 0, а затем создает запрос на перемещение типа R_X86_64_PC32. Он инструктирует компоновщик исправить значение 0 с реальным значением смещения. Но он не может этого сделать, поскольку значение смещения не будет помещаться внутри подписанного 32-битного целого и, следовательно, выйдет из строя.

При использовании модели памяти medium все выглядит так:

movabsq $bar.1535, %r10
...
movl    %eax, baz_+4(%rip)
...
.local  bar.1535
.largecomm      bar.1535,2575411200,32
...
.comm   baz_,12,16

Сначала выполняется 64-битная команда немедленного перемещения (длиной 10 байт), чтобы поместить 64-битное значение, которое представляет адрес bar.1535, в регистр R10. Память для символа bar.1535 выделяется с помощью директивы .largecomm и, следовательно, заканчивается в секции .lbss ELF exectuable. .lbss используется для хранения символов, которые могут не соответствовать в первых двух GiB (и, следовательно, не должны быть адресованы с использованием 32-разрядных инструкций или RIP-относительной адресации), в то время как меньшие вещи идут в .bss (baz_ все еще выделяется с помощью .comm, а не .largecomm). Поскольку раздел .lbss размещается после раздела .bss в компоновщике ELF script, baz_ не будет недоступен при использовании 32-разрядной адресации, связанной с RIP.

Все режимы адресации описаны в System V ABI: AMD64 Architecture Processor Supplement. Это тяжелое техническое чтение, но оно должно читать для тех, кто действительно хочет понять, как работает 64-разрядный код для большинства x86_64 Unixes.

Если вместо этого используется массив ALLOCATABLE, gfortran выделяет кучную память (скорее всего, реализован как анонимная карта памяти с учетом большого размера выделения):

movl    $2575411200, %edi
...
call    malloc
movq    %rax, %rdi

Это в основном RDI = malloc(2575411200). С этого момента элементы bar получают путем использования положительных смещений от значения, хранящегося в RDI:

movl    51190040(%rdi), %eax
movl    %eax, baz_+4(%rip)

Для мест с более чем 2 ГБ с начала bar используется более сложный метод. Например. для реализации b = bar(12,144*144*450) gfortran испускает:

; Some computations that leave the offset in RAX
movl    (%rdi,%rax), %eax
movl    %eax, baz_+4(%rip)

Этот код не влияет на модель памяти, поскольку ничего не предполагается относительно адреса, в котором будет выполняться динамическое распределение. Кроме того, поскольку массив не передается, дескриптор не создается. Если вы добавите еще одну функцию, которая принимает массив предполагаемой формы и передаст ей bar, дескриптор для bar создается как автоматическая переменная (т.е. В стеке foo). Если массив сделан статическим с атрибутом SAVE, дескриптор помещается в раздел .bss:

movl    $bar.1580, %edi
...
; RAX still holds the address of the allocated memory as returned by malloc
; Computations, computations
movl    -232(%rax,%rdx,4), %eax
movl    %eax, baz_+4(%rip)

Первый шаг подготавливает аргумент вызова функции (в моем примере примера call boo(bar), где boo имеет интерфейс, объявляющий его как принимающий массив формы). Он перемещает адрес дескриптора массива bar в EDI. Это 32-битный немедленный ход, поэтому ожидается, что дескриптор будет находиться в первых двух гигабайтах. Действительно, он выделяется в .bss в моделях памяти small и medium следующим образом:

.local  bar.1580
.comm   bar.1580,72,32
.local  bar.1580
.comm   bar.1580,72,32

Ответ 2

Нет, большие статические массивы (как ваш bar) могут превышать предел, если вы не используете -mcmodel=medium. Но allocatables лучше, конечно. Для allocatables только дескриптор массива должен вписываться в 2 ГБ, а не весь массив.

Из справки GCC:

-mcmodel=small
Generate code for the small code model: the program and its symbols must be linked in the lower 2 GB of the address space. Pointers are 64 bits. Programs can be statically or dynamically linked. This is the default code model. 
-mcmodel=kernel
Generate code for the kernel code model. The kernel runs in the negative 2 GB of the address space. This model has to be used for Linux kernel code. 
-mcmodel=medium
Generate code for the medium model: The program is linked in the lower 2 GB of the address space but symbols can be located anywhere in the address space. Programs can be statically or dynamically linked, but building of shared libraries are not supported with the medium model. 
-mcmodel=large
Generate code for the large model: This model makes no assumptions about addresses and sizes of sections. Currently GCC does not implement this model.