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 массивы и тот, что без) были более или менее эквивалентны...

2 ответа

Решение

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

С small 32-разрядные инструкции адресации модели памяти используются для адресации символов в x86_64. Это делает код меньше и быстрее. Некоторый сборочный вывод при использовании 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 ОБЩИЙ блок распределяется впоследствии. Так как bar.1535 очень большой, baz_ в конечном итоге более 2 ГиБ с начала .bss раздел. Это создает проблему во втором movl инструкция, поскольку не 32-битное (подписанное) смещение от RIP следует использовать для решения b переменная, где значение EAX должен быть перемещен в. Это обнаруживается только во время соединения. Сам ассемблер не знает соответствующего смещения, так как он не знает, каково значение указателя инструкции (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 секция эльфийская .lbss используется для хранения символов, которые могут не помещаться в первые 2 ГиБ (и, следовательно, не должны быть адресованы с использованием 32-битных инструкций или RIP-относительной адресации), в то время как меньшие вещи идут на .bss (baz_ по-прежнему выделяется с помощью .comm и не .largecomm). Так как .lbss раздел размещен после .bss раздел в скрипте компоновщика ELF, baz_ не будет недоступен при использовании 32-битной RIP-адресации.

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

Когда 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-битный немедленный ход, поэтому ожидается, что дескриптор будет в первых 2 ГиБ. Действительно, он выделяется в .bss в обоих small а также medium Модели памяти как это:

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

Нет, большие статические массивы (как ваш bar) может превышать лимит, если вы не используете -mcmodel=medium, Но выделяемые ресурсы лучше, конечно. Для выделяемых объектов только 2 дескриптора массива должны умещаться в 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.
Другие вопросы по тегам