Что происходит, когда команда mov вызывает ошибку страницы с отключенными прерываниями на x86?

Недавно я столкнулся с проблемой в произвольном ядре Linux (2.6.31.5, x86), где copy_to_user периодически не копировал любые байты в пространство пользователя. Он вернет количество байтов, переданных ему, что указывает на то, что он ничего не скопировал. После проверки кода мы обнаружили, что код отключил прерывания при вызове copy_to_user, который нарушает его. После исправления этого вопроса проблема прекратилась. Поскольку проблема возникла нечасто, мне нужно доказать, что отключение прерываний вызвало проблему.

Если вы посмотрите на фрагмент кода ниже из arch/x86/lib/usercopy_32.c rep; movsl копирует слова в пользовательское пространство по счету в CX. Размер обновляется CX при выходе. CX будет 0, если movsl выполняется правильно. Поскольку CX не равен нулю, movs? инструкции не должны выполняться, чтобы соответствовать определению copy_to_user и наблюдаемому поведению.

/* Generic arbitrary sized copy.  */
#define __copy_user(to, from, size)                 \
do {                                    \
    int __d0, __d1, __d2;                       \
    __asm__ __volatile__(                       \
        "   cmp  $7,%0\n"                   \
        "   jbe  1f\n"                  \
        "   movl %1,%0\n"                   \
        "   negl %0\n"                  \
        "   andl $7,%0\n"                   \
        "   subl %0,%3\n"                   \
        "4: rep; movsb\n"                   \
        "   movl %3,%0\n"                   \
        "   shrl $2,%0\n"                   \
        "   andl $3,%3\n"                   \
        "   .align 2,0x90\n"                \
        "0: rep; movsl\n"                   \
        "   movl %3,%0\n"                   \
        "1: rep; movsb\n"                   \
        "2:\n"                          \
        ".section .fixup,\"ax\"\n"              \
        "5: addl %3,%0\n"                   \
        "   jmp 2b\n"                   \
        "3: lea 0(%3,%0,4),%0\n"                \
        "   jmp 2b\n"                   \
        ".previous\n"                       \
        ".section __ex_table,\"a\"\n"               \
        "   .align 4\n"                 \
        "   .long 4b,5b\n"                  \
        "   .long 0b,3b\n"                  \
        "   .long 1b,2b\n"                  \
        ".previous"                     \
        : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2)   \
        : "3"(size), "0"(size), "1"(to), "2"(from)      \
        : "memory");                        \
} while (0)

2 идеи, которые у меня есть:

  • когда прерывания отключены, ошибка страницы не возникает и затем rep; варисторы? пропускается, ничего не делая. Возвращаемое значение тогда будет CX или количество, не скопированное в пользовательское пространство, так как определение и наблюдаемое поведение.
  • Ошибка страницы, но linux не может ее обработать, поскольку прерывания отключены, поэтому обработчик ошибок страницы пропускает инструкцию, хотя я не знаю, как это сделает обработчик ошибок страницы. Опять же, в этом случае CX останется неизмененным, и возвращаемое значение будет правильным.

Может ли кто-нибудь указать мне разделы в руководствах Intel, которые определяют это поведение, или указать мне на любой дополнительный источник Linux, который может быть полезен?

Ответы

Ответ 1

Я нашел ответ. Мое предложение № 2 было правильным, и механизм был прямо перед моим лицом. Ошибка страницы происходит, но механизм fixup_exception используется для предоставления механизма исключения/продолжения. В этом разделе добавляются записи в таблицу обработчика исключений:

    ".section __ex_table,\"a\"\n"               \
    "   .align 4\n"                 \
    "   .long 4b,5b\n"                  \
    "   .long 0b,3b\n"                  \
    "   .long 1b,6b\n"                  \
    ".previous"                     \

Это говорит: если IP-адрес является первой записью, и в обработчике ошибок встречается исключение, установите IP-адрес на второй адрес и продолжайте.

Итак, если исключение происходит при "4:", перейдите к "5:". Если исключение происходит при "0:", то переходите к "3:", и если исключение происходит при переходе "1:" на "6:".

Пропущенный фрагмент находится в do_page_fault() в arch/x86/mm/fault.c:

/*
 * If we're in an interrupt, have no user context or are running
 * in an atomic region then we must not take the fault:
 */
if (unlikely(in_atomic() || !mm)) {
    bad_area_nosemaphore(regs, error_code, address);
    return;
}

in_atomic возвращается true, потому что мы находимся в блокировке write_lock_bh()! bad_area_nosemaphore в конечном итоге делает исправление.

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

Ответ 2

Ошибки страниц не являются прерываниями масок. Фактически, они не являются технически прерываниями вообще, а скорее исключениями, хотя я согласен, что разница более семантическая.

Причина, по которой ваша copy_to_user не удалась, когда вы вызывали ее в атомарном контексте с отключенными прерываниями, состоит в том, что код имеет явную проверку для этого.

См. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L575