распределение регистров, как использовать и разливать сохраненные регистры вызывающего абонента

Я узнал, что если какой-либо из caller saved registers (rax rdx rcx rsi rdi r8 r9 r10 r11) используется вызываемым пользователем, он должен быть сохранен до и восстановлен после инструкции call вызывающим.

В следующем примере

int read();
void print(int i);

int main()
{
    int a = read();
    int b = read();
    int c = read();
    int d = read();
    int e = read();
    int f = read();
    int g = read();
    print(a);
    print(b);
    print(c);
    print(d);
    print(e);
    print(f);
    print(g);
}

Примечание

  1. Переменные a - g должны использовать все callee saved registers (rbp rsp rbx r12 r13 r14 r15). И мы не можем использовать одновременно rbp или rsp, поскольку для адресации памяти стека необходимо использовать любой из них.

  2. read и print взяты из некоторого внешнего модуля компиляции. Таким образом, мы действительно не знаем об использовании регистров сохранения вызывающей стороны, когда мы компилируем текущий модуль компиляции, в частности, во время выделения регистров для функции main.

В godbolt с -O3 он компилируется в следующее

main:
  pushq %r15
  pushq %r14
  pushq %r13
  pushq %r12
  pushq %rbp
  pushq %rbx
  subq $24, %rsp # spill here
  call read()
  movl %eax, 12(%rsp) # spill here
  call read()
  movl %eax, %ebx
  call read()
  movl %eax, %r15d
  call read()
  movl %eax, %r14d
  call read()
  movl %eax, %r13d
  call read()
  movl %eax, %r12d
  call read()
  movl 12(%rsp), %edi
  movl %eax, %ebp
  call print(int)
  movl %ebx, %edi
  call print(int)
  movl %r15d, %edi
  call print(int)
  movl %r14d, %edi
  call print(int)
  movl %r13d, %edi
  call print(int)
  movl %r12d, %edi
  call print(int)
  movl %ebp, %edi
  call print(int)
  addq $24, %rsp
  xorl %eax, %eax
  popq %rbx
  popq %rbp
  popq %r12
  popq %r13
  popq %r14
  popq %r15
  ret

Примечание

  1. Переменная a переносится в 12(%rsp).

  2. Нам не нужно проливать какие-либо caller saved registers, поскольку они вообще не используются, что здесь оказывается более эффективным.

Мои вопросы

  1. Похоже, нам действительно не нужно заниматься проливанием caller saved registers, если мы их не используем. Таким образом, когда мы должны использовать caller saved registers?

  2. Для таких вызываемых абонентов, как read и print, поскольку мы не знаем об их использовании регистров, как мы должны сделать разлив для caller saved registers?

Спасибо




Ответы (2)


Похоже, что сбивающая с толку и неинтуитивная терминология сохраненная / сохраненная вызывающая сторона ввела вас в заблуждение, заставив думать, что каждый регистр всегда должен кем-то где-то сохраняться. См. Что такое сохраненные регистры вызываемого и вызывающего абонентов? - Сохранение вызова по сравнению с сокращением вызова более полезно как с точки зрения простоты запоминания, так и в качестве ментальной модели. Это нормально, когда значения уничтожаются, как функция arg.

Похоже, нам действительно не нужно иметь дело с утечкой сохраненных регистров вызывающего абонента, если мы их не используем.

Обратите внимание, что ваша функция действительно использует пару регистров с закрытым вызовом (вызывающий сохранен): он использует RDI для передачи аргумента в print(int) и обнуляет RAX как возвращаемое значение main.

В случаях, когда у него есть значение в регистре с закрытым вызовом , которое должно выжить при вызове функции, GCC решил mov сохранить это значение в регистре с сохранением вызова. например когда read() возвращается, его возвращаемое значение находится в EAX, которое будет уничтожено при следующем вызове. Таким образом, он использует mov %eax, %ebp или что-то еще, чтобы сохранить его в регистре с сохранением вызовов, или переносит его в 12(%rsp).

(Обратите внимание, что GCC использовал push / pop для сохранения / восстановления значений вызывающих его регистров с сохранением вызовов, которые он использует.)

Стратегия генерации кода GCC по умолчанию заключается в том, чтобы сохранять / восстанавливать регистры с сохранением вызовов для хранения значений между вызовами, а не перетекать в память внутри этой функции. Обычно это хорошо для менее тривиальных случаев, особенно для вызовов внутри циклов. См. Почему компиляторы настаивают на использовании вызываемого сохраненный регистр здесь?, чтобы узнать об этом подробнее.

И мы не можем использовать и rbp, и rsp, так как любой из них должен использоваться для адресации памяти стека.

Также неверно: с -fomit-frame-pointer (на большинстве уровней оптимизации) RBP - это просто еще один регистр с сохранением вызовов. Ваша функция использует его для сохранения возвращаемого значения read. (EBP - это младшие 32 бита RBP).

person Peter Cordes    schedule 02.10.2020
comment
Спасибо за ответы. Признаюсь, соглашение об именах меня немного ввело в заблуждение. Основываясь на вашем объяснении, call clobbered registers следует использовать только для хранения временных значений между вызовами, тогда как call preserved registers предназначены для хранения чего-то вроде переменных. - person Lin; 02.10.2020
comment
Некоторые дополнения. 1. Таким образом, при распределении регистров, т. Е. Отображении переменных в регистры, мы должны использовать только call preserved registers? 2. И когда мы вызываем процедуры из другого модуля компиляции, я считаю, что нам не нужно беспокоиться о сохранении / восстановлении засоренных регистров вызова. 3. Если это так, то, когда мы проектируем архитектуру, мы, естественно, должны выбрать больше call preserved registers для большего количества переменных? - person Lin; 02.10.2020
comment
@Lin: прочтите Что такое сохраненные регистры вызывающего и вызывающего абонентов? - там я привел больше примеров и подробностей. 1. Нет, в листовой функции сначала используйте регистры с закрытыми вызовами, надеюсь, не нужно ничего сохранять / восстанавливать или выделять какое-либо пространство стека. Или, если у вас есть переменная, которая существует только между двумя вызовами функций, например, счетчик цикла для цикла, который не содержит никаких вызовов, используйте для нее регистр с закрытием вызовов. - person Peter Cordes; 03.10.2020
comment
3. нет, регистры с закрытыми вызовами тоже ценны. И это не особенность ISA; разработка соглашения о вызовах осуществляется отдельно от оборудования. Например, x86-64 имеет два основных: x86-64 System V и Windows x64. См. Почему Windows64 использует другое соглашение о вызовах, чем все другие операционные системы на x86-64?, где приведены некоторые ссылки на сообщения архива списка рассылки о том, как / почему так оно и было задумано. См. Также Почему бы не хранить параметры функций в векторных регистрах XMM? для получения дополнительной информации о компромиссе между энергозависимыми и энергонезависимыми регистрами. - person Peter Cordes; 03.10.2020

Я узнал, что если какой-либо из сохраненных регистров вызывающего абонента (rax rdx rcx rsi rdi r8 r9 r10 r11) используется вызываемым пользователем, то он должен быть сохранен до и восстановлен после инструкции вызова вызывающим абонентом.

должно быть

Я узнал, что если какой-либо из сохраненных регистров вызывающего абонента (rax rdx rcx rsi rdi r8 r9 r10 r11) используется вызывающим абонентом, он должен быть сохранен до и восстановлен после инструкции вызова вызывающим абонентом.

Регистры сохранения вызывающего абонента - это те регистры, которые могут быть заблокированы любой вызываемой функцией. Вы не знаете наверняка, использует ли их какой-либо конкретный вызываемый объект, поэтому вы должны предполагать худшее. Однако вызывающему абоненту необходимо сохранить их только в том случае, если вызывающий абонент их использует. Если нет, то вам все равно, что их могут заткнуть.

person Chris Dodd    schedule 01.10.2020
comment
Что ж, вам не нужно сохранять и восстанавливать реестр как таковой; только если он содержит значение, которое необходимо сохранить. И в идеале вы можете спроектировать свой компилятор так, чтобы значения, которые необходимо сохранять при вызове функции, никогда не помещались в такие регистры, а вместо этого помещались в регистры, сохраненные вызываемым пользователем, или в память. - person Nate Eldredge; 02.10.2020