Почему код ядра не может использовать красную зону

Настоятельно рекомендуется при создании 64-разрядного ядра (для платформы x86_64) указывать компилятору не использовать 128-байтовую красную зону, как это делает пользовательский интерфейс ABI. (Для GCC флаг компилятора -mno-red-zone).

Ядро не будет защищено от прерываний, если оно включено.

Но почему это?

4 ответа

Решение

Цитирование из AMD64 ABI:

128-байтовая область за пределами местоположения, на которое указывает%rsp, считается зарезервированной и не должна изменяться обработчиками сигналов или прерываний. Поэтому функции могут использовать эту область для временных данных, которые не нужны при вызовах функций. В частности, листовые функции могут использовать эту область для всего кадра стека, вместо того, чтобы корректировать указатель стека в прологе и эпилоге. Эта область известна как красная зона.

По сути, это оптимизация - компилятор пользовательского пространства точно знает, сколько красной зоны используется в любой момент времени (в простейшей реализации, весь размер локальных переменных) и может настроить %rsp соответственно перед вызовом подфункции.

Особенно в конечных функциях, это может дать некоторые преимущества в производительности без необходимости настройки %rsp как мы можем быть уверены, незнакомый код не будет выполняться в функции. (Обработчики сигналов POSIX можно рассматривать как форму сопрограммы, но вы можете указать компилятору настроить регистры перед использованием переменных стека в обработчике сигналов).

В пространстве ядра, когда вы начинаете думать о прерываниях, если эти прерывания делают какие-либо предположения о %rspони, скорее всего, будут неправильными - нет уверенности в отношении использования Красной зоны. Таким образом, вы либо предполагаете, что все это грязно, и излишне тратят пространство стека (эффективно работает с гарантированной локальной переменной размером 128 байт в каждой функции), либо вы гарантируете, что прерывания не делают предположений о %rsp - что сложно.

В пользовательском пространстве контекстные переключатели + 128-байтовое перераспределение стека обрабатывают это для вас.

Можно использовать red-zone в контекстах типа ядра. IDTentry может указывать индекс стека (ist), равный 0,7, где 0 - особенный бит. TSS содержит таблицу этих стеков. 1..7 загружаются и используются для начальных регистров, сохраненных исключением / прерыванием, и не вкладываются. Если вы разделяете различные записи об исключениях по приоритетам (например, NMI является наивысшим и может произойти в любое время) и рассматриваете эти стеки как батуты, вы можете безопасно обрабатывать красные зоны в контекстах типа ядра. Таким образом, вы можете вычесть 128 из сохраненного указателя стека, чтобы получить пригодный для использования стек ядра, прежде чем включать прерывания или код, который может вызвать исключения.

Стек с нулевым индексом ведет себя более обычным образом, помещая стек, флаги, pc, ошибку в существующий стек, когда нет перехода привилегий.

Код в батуте должен быть осторожен (да, это ядро), чтобы не генерировать другие исключения, пока он дезинфицирует состояние машины, но обеспечивает хорошее, безопасное место для обнаружения патологического вложения ядра, повреждения стека и т. Д... [ извините, что ответил так поздно, заметил это, когда искал что-то еще].

В пространстве ядра вы используете тот же стек, который используют прерывания. Когда происходит прерывание, ЦП выдвигает адрес возврата и RFLAGS. Это забивает 16 байтов ниже rsp, Даже если вы захотите написать обработчик прерываний, который предполагал бы, что полные 128 байтов красной зоны были полезны, это было бы невозможно.


Возможно, вы могли бы иметь внутренний ABI ядра, который имел маленькую красную зону от rsp-16 в rsp-48 или что-то. (Небольшой, потому что стек ядра полезен, и большинству функций в любом случае не нужно много красной зоны.)

Обработчики прерываний должны sub rsp, 32 перед нажатием любых регистров. (и восстановить его раньше iret).

Эта идея не будет работать, если обработчик прерываний сам может быть прерван до его запуска sub rsp, 32или после восстановления rsp перед iret, Там будет окно уязвимости, где ценные данные в rsp .. rsp-16,


Другая практическая проблема с этой схемой состоит в том, что AFAIK gcc не имеет настраиваемых параметров красной зоны. Это или включено или выключено. Таким образом, вам нужно добавить поддержку ядра red-zone в gcc / clang, если вы хотите воспользоваться этим.

Даже если это было безопасно от вложенных прерываний, преимущества довольно малы. Сложность доказательства того, что это безопасно в ядре, может сделать его не стоящим. (И, как я уже сказал, я совсем не уверен, что это можно реализовать безопасно, потому что я думаю, что возможны вложенные прерывания.)


(Кстати, см. Вики-тег x86 для ссылок на ABI, документирующих красную зону, и другие вещи.)

Приведу пример цитаты из википедии:

Хорошо известно, что красная зона вызывает проблемы у разработчиков ядра x86-64, поскольку сам ЦП не учитывает красную зону при вызове обработчиков прерываний. Это приводит к тонкой поломке ядра, поскольку ABI противоречит поведению процессора.

В своем ядре я использую функцию Linux memcpy() c:

      void *memcpy(void *dest, const void *src,
                size_t count)
{
    char *tmp = dest;
    const char *s = src;

    while (count--)
        *tmp++ = *s++;
    return dest;
}

А разборка такая:

      0000000000000000 <memcpy>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 8d 05 f9 ff ff ff    lea    -0x7(%rip),%rax        # 8 <memcpy+0x8>
   f:   49 bb 00 00 00 00 00    movabs $0x0,%r11
  16:   00 00 00 
  19:   4c 01 d8                add    %r11,%rax
  1c:   48 89 7d e8             mov    %rdi,-0x18(%rbp)
  20:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
  24:   48 89 55 d8             mov    %rdx,-0x28(%rbp)
  28:   48 8b 45 e8             mov    -0x18(%rbp),%rax
  2c:   48 89 45 f8             mov    %rax,-0x8(%rbp)
  30:   48 8b 45 e0             mov    -0x20(%rbp),%rax
  34:   48 89 45 f0             mov    %rax,-0x10(%rbp)
  38:   eb 1d                   jmp    57 <memcpy+0x57>
  3a:   48 8b 55 f0             mov    -0x10(%rbp),%rdx
  3e:   48 8d 42 01             lea    0x1(%rdx),%rax
  42:   48 89 45 f0             mov    %rax,-0x10(%rbp)
  46:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  4a:   48 8d 48 01             lea    0x1(%rax),%rcx
  4e:   48 89 4d f8             mov    %rcx,-0x8(%rbp)
  52:   0f b6 12                movzbl (%rdx),%edx
  55:   88 10                   mov    %dl,(%rax)
  57:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  5b:   48 8d 50 ff             lea    -0x1(%rax),%rdx
  5f:   48 89 55 d8             mov    %rdx,-0x28(%rbp)
  63:   48 85 c0                test   %rax,%rax
  66:   75 d2                   jne    3a <memcpy+0x3a>
  68:   48 8b 45 e8             mov    -0x18(%rbp),%rax
  6c:   5d                      pop    %rbp
  6d:   c3                      retq

Обратите внимание на инструкцию в 1c to 24, три аргумента, хранящиеся в стеке с помощью «mov», но не «push», так же, как 2c и 34, являются двумя локальными переменными.

А теперь проблема. Я скомпилировал свое ядро ​​​​x86_64 на Ubuntu с gcc по умолчанию x64 abi(sysv amd64 abi, неявная красная зона). При запуске этой функции, вызванной exec, обязательно будет запущено копирование при записи (означает, что сначала вызовет исключение ошибки страницы), адрес переменных и %RSP выглядят следующим образом: снимок экрана сеанса отладки 1

Вы можете видеть, что %RSP расположен НАД сохраненными аргументами и локальными переменными, так что угадайте, что произойдет, когда возникнет исключение на машине x86_64 — автосохранение процессором не менее 5 регистров в стеке — они переопределяют аргументы и локальные переменные.

А потом скомпилировал с опцией -mno-red-zone, начальная часть дизассемблирования:

      0000000000000000 <memchr>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 83 ec 28             sub    $0x28,%rsp
   c:   48 8d 05 f9 ff ff ff    lea    -0x7(%rip),%rax        # c <memchr+0xc>

Обратите внимание на разницу с первым? Он сохраняет пространство стека аргументов и локальных переменных с

      8:   48 83 ec 28             sub    $0x28,%rsp

И текущий результат:снимок экрана сеанса отладки 2. Теперь %RSP находится НИЖЕ аргументов и локальных переменных.

Таким образом, основная причина заключается в следующем: в листовой функции в обычном случае нет необходимости настраивать %RSP на вершину стека, поэтому с механизмом красной зоны %RSP не будет корректироваться. Но в ядре код ядра и код исключения/прерывания совместно используют стек ядра (если вы не подготовите изолированный стек для исключения/прерывания, для процессора X86_64 это IST), когда конечная функция прерывается, args и localvars будут переопределены

Другие вопросы по тегам