Что делает инструкция ljmp в системном вызове форка ядра Linux?

Я изучаю исходный код ядра Linux (старая версия 0.11v). Когда я проверил системный вызов fork, я обнаружил некоторый ассемблерный код для переключения контекста, например:

/*
 * switch_to(n) should switch tasks to task nr n, first
 * checking that n isn't the current task, in which case it does nothing.
 * This also clears the TS-flag if the task we switched to has used
 * tha math co-processor latest.
 */
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,current\n\t" \
    "ljmp *%0\n\t" \
    "cmpl %%ecx,last_task_used_math\n\t" \
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

Я предполагаю, что "ljmp %0\n\t" будет работать для изменения TSS и LDT. Я знаю, что инструкции ljmp нужны два параметра, например ljmp $section, $offset. Я думаю, что инструкция ljmp должна использовать _TSS(n), xx. Нам не нужно указывать значимое значение смещения, потому что процессор изменит регистр процессора, включая eip, для новой задачи.

  1. Я не знаю, как ljmp %0 работает как ljmp $section, $offset и почему в этой инструкции используется %0. Является ли %0 просто адресом __tmp.a?

  2. ЦП может сохранить регистр EIP в TSS для старой задачи при выполнении инструкции ljmp. Правильно ли я понимаю, что значением EIP для старой задачи является адрес "cmpl %%ecx,_last_task_used_math\n\t"?


person bongsu    schedule 18.11.2015    source источник
comment
Это ужасно читать, и некоторые комментарии Линуса могли бы быть хорошими. ljmp %0 перейдет к 48-битному адресу, содержащемуся в памяти по адресу %0. Так что эффективно он будет ljmp по адресу, содержащемуся в памяти по адресу __tmp. Вы заметите, что movw %%dx,%1 фактически инициализирует __tmp.b значением _TSS(n). _TSS(n) будет дескриптором сегмента для шлюза задачи. Вы заметите, что %0 (__tmp.a) не инициализирован. В этом нет необходимости, поскольку смещение (которое представляет __tmp.a) игнорируется, когда вы ljmp проходите через шлюз задачи. Фактически вы делаете ljmp для сегмента: смещение _TSS (n): мусор.   -  person Michael Petch    schedule 18.11.2015
comment
ljmp _TSS(n):garbage, когда _TSS(n) представляет селектор ворот задачи, будет переключать задачу на основе селектора задач, игнорируя смещение (поэтому не нужно ничего устанавливать), и продолжит выполнение инструкции после ljmp в новом контексте задачи.   -  person Michael Petch    schedule 18.11.2015
comment
cmpl %%ecx,_last_task_used_math будет выполняться в контексте новой задачи после ljmp. Я не смотрел исходный код старого ядра, но кажется, что _last_task_used_math — это идентификатор последней задачи, в которой использовалась математическая инструкция. Если он отличается от текущего идентификатора задачи, инструкция clts не используется.   -  person Michael Petch    schedule 18.11.2015
comment
Также обратите внимание, что от переключения задач TSS в Linux отказались, так что это представляет только исторический интерес.   -  person Jester    schedule 18.11.2015
comment
Да, @Jester, я только комментировал это старое ядро, так как, похоже, это то, что интересовало ОП. Linux прошел долгий путь с 0.11;)   -  person Michael Petch    schedule 18.11.2015
comment
Да, и я видел, что это старая версия, но, возможно, ОП не знает, что от этого конкретного подхода полностью отказались.   -  person Jester    schedule 18.11.2015
comment
@MichaelPetch Насколько я вижу, исходный код Linux 0.11 содержит комментарий перед этим определением макроса. Если это так, ОП должен был скопировать его вместе с кодом. Спасибо за объяснение ворот задач, это одна из вещей, о которых я никогда не читал, и именно то, что я не мог объяснить.   -  person    schedule 18.11.2015
comment
@MichaelPetch спасибо за ваш ответ. но я не понимаю, как ljmp %0 будет ljmp TSS(n):мусор.   -  person bongsu    schedule 19.11.2015
comment
@MichaelPetch Думаю, мне нужно изменить код, например ljmp %1:%0.   -  person bongsu    schedule 19.11.2015
comment
@Rhymoid Извините, я обновил комментарий.   -  person bongsu    schedule 19.11.2015
comment
Я создал вики сообщества для обработки информативных комментариев @MichaelPetch.   -  person    schedule 19.11.2015
comment
ljmp TSS(n):garbage было упрощением, и я должен извиниться. Сам указатель на самом деле находится в tmp . Используемая форма ljmp представляет собой косвенный длинный переход, где один параметр является указателем на ячейку памяти, которая содержит указатель для перехода. Интересно то, что использовавшийся ассемблер (2 десятилетия назад) был более слабым в отношении синтаксиса для ljmp. Чтобы избежать путаницы, если вы хотите обозначить косвенный дальний jmp через указатель, он будет выглядеть как ljmp *%0 . Звездочка говорит, что мы переходим к адресу, содержащемуся в ячейке памяти (%0 — это адрес tmp)   -  person Michael Petch    schedule 19.11.2015
comment
Используя современный ассемблер GNU, он на самом деле предупредит вас с помощью этого Предупреждение: косвенный ljmp без * . Я думаю, что идея с предупреждением хороша. Хотя ассемблер (на x86) может сделать вывод, что вы имеете в виду непрямой длинный переход на основании одного операнда памяти, он предполагает, что лучше добавить звездочку при обозначении непрямых переходов, что могло бы избежать путаницы.   -  person Michael Petch    schedule 19.11.2015
comment
@Rhymoid: Как вы можете сказать, я, вероятно, не смотрел на рассматриваемый код ядра (спасибо за внимание) - иначе я бы знал, что у Линуса есть комментарии к коду. Я думаю, что в этом случае встроенные комментарии в коде могли бы быть полезны, поскольку неясно, что именно происходит с ljmp (при использовании целевого адреса, который использует дескриптор задачи)   -  person Michael Petch    schedule 19.11.2015


Ответы (1)


Что вообще означает этот синтаксис?

Этот нечитаемый беспорядок представляет собой Extended ASM GCC, который имеет общий формат

 asm [volatile] ( AssemblerTemplate
                : OutputOperands
              [ : InputOperands
              [ : Clobbers ] ] )

В этом случае оператор __asm__ содержит только AssemblerTemplate и InputOperands. Часть входных операндов объясняет, что означают %0 и %1 и как ecx и edx получают свое значение:

  • Первый входной операнд — "m" (*&__tmp.a), поэтому %0 становится mадресом памяти __tmp.a (честно говоря, я не уверен, зачем здесь нужен *&).
  • Второй входной операнд — "m" (*&__tmp.b), поэтому %1 становится mадресом памяти __tmp.b.
  • Третий входной операнд — "d" (_TSS(n)), поэтому регистр DX будет содержать _TSS(n) при запуске этого кода.
  • Четвертый входной операнд — "c" ((long) task[n]), поэтому регистр ECX будет содержать task[n] при запуске этого кода.

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

    cmpl %ecx, _current
    je 1f

    movw %dx, __tmp.b          ;; the address of __tmp.b
    xchgl %ecx, _current
    ljmp __tmp.a               ;; the address of __tmp.a

    cmpl %ecx, _last_task_used_math
    jne 1f
    clts
1:

Как ljmp %0 вообще может работать?

Обратите внимание, что существует две формы инструкции ljmp (также известной как jmpf). Тот, который вы знаете (код операции EA), принимает два непосредственных аргумента: один для сегмента, один для смещения. Используемый здесь (опкод FF /5) отличается: аргументы сегмента и адреса находятся не в потоке кода, а где-то в памяти, а инструкция указывает на адрес.

В этом случае аргумент ljmp указывает в начале на структуру __tmp. Первые четыре байта (__tmp.a) содержат смещение, а следующие два байта (младшая половина __tmp.b) содержат сегмент.

Этот косвенный ljmp __tmp.a будет эквивалентен ljmp [__tmp.b]:[__tmp.a], за исключением того, что ljmp segment:offset может принимать только непосредственные аргументы. Если вы хотите переключиться на произвольный TSS без самомодифицирующегося кода (что было бы ужасной идеей), используйте косвенную инструкцию.

Также обратите внимание, что __tmp.a никогда не инициализируется. Мы можем предположить, что _TSS(n) относится к шлюзу задачи (потому что именно так вы выполняете переключение контекста с помощью TSS), а смещение для переходов «сквозь» шлюз задачи игнорируется.

Куда делся старый указатель инструкций?

Этот фрагмент кода не сохраняет старый EIP в TSS.

(Я предполагаю, что после этого момента, но я думаю, что это предположение разумно.)

Старый EIP хранится в стеке пространства ядра, соответствующем старой задаче.

Linux 0.11 выделяет кольцевой стек 0 (т. е. стек для ядра) для каждой задачи (см. функцию copy_process в fork.c, которая инициализирует TSS). Когда во время выполнения задачи A происходит прерывание, старый EIP сохраняется в стеке пространства ядра, а не в стеке пользовательского пространства. Если ядро ​​решает переключиться на задачу B, стек пространства ядра также переключается. Когда ядро ​​в конце концов переключается обратно на задачу А, этот стек переключается обратно, и через iret мы можем вернуться туда, где мы были в задаче А.

person Community    schedule 19.11.2015
comment
Приятным дополнением было бы объяснение того, как математический сопроцессор связан с флагом TS. - person ; 19.11.2015
comment
Спасибо, что формализовали мои комментарии и поместили их в вики сообщества (престижность). Единственная причина, по которой я этого не сделал, заключалась в том, что я не смотрел код ядра, чтобы убедиться, что на самом деле происходит. Что касается сопроцессора, то при переключении задач устанавливается флаг TS, так что следующая операция сопроцессора выдает исключение, которое может быть перехвачено. Когда исключение перехвачено, ядро ​​​​может сохранить состояние сопроцессора в стеке. Если предыдущая задача и текущая задача совпадают, вы очищаете ее, потому что нет необходимости сохранять состояние сопроцессора и избегать создания исключения am. - person Michael Petch; 19.11.2015