Почему я могу выполнять операции с плавающей запятой внутри модуля ядра Linux?
Я работаю на x86 CentOS 6.3 (ядро v2.6.32) системы.
В качестве эксперимента я скомпилировал следующую функцию в модуль драйвера персонажа, чтобы увидеть, как ядро Linux реагирует на операции с плавающей запятой.
static unsigned floatstuff(void){
float x = 3.14;
x *= 2.5;
return x;
}
...
printk(KERN_INFO "x: %u", x);
Код скомпилирован (чего не ожидалось), поэтому я вставил модуль и проверил журнал dmesg
, Журнал показал: x: 7
,
Это кажется странным; Я думал, что вы не можете выполнять операции с плавающей запятой в ядре Linux - за исключением некоторых исключений, таких как kernel_fpu_begin()
, Как модуль выполнял операцию с плавающей запятой?
Это потому что я на процессоре x86?
4 ответа
Я думал, что вы не можете выполнять операции с плавающей запятой в ядре Linux
Вы не можете безопасно: отказ от использования kernel_fpu_begin()
/ kernel_fpu_end()
не означает, что инструкции FPU будут ошибочными (по крайней мере, на x86).
Вместо этого он будет молча повреждать состояние FPU пользовательского пространства. Это плохо; не делай этого
Компилятор не знает что kernel_fpu_begin()
означает, что он не может проверять / предупреждать о коде, который компилируется в инструкции FPU за пределами областей начала FPU.
Может существовать режим отладки, когда ядро отключает инструкции SSE, x87 и MMX вне kernel_fpu_begin
/ end
регионы, но это будет медленнее и не будет сделано по умолчанию.
Это возможно, однако: установка CR0::TS = 1
делает ошибки инструкции x87, поэтому переключение контекста FPU возможно, и есть другие биты для SSE и AVX.
Есть много способов, чтобы ошибочный код ядра вызывал серьезные проблемы. Это только один из многих. В C вы почти всегда знаете, когда используете плавающую точку (если опечатка не приводит к 1.
константа или что-то в контексте, который на самом деле компилируется).
Почему архитектурное состояние FP отличается от целочисленного?
Linux должен сохранять / восстанавливать целочисленное состояние каждый раз, когда он входит / выходит из ядра. Весь код должен использовать целочисленные регистры (за исключением гигантского линейного блока вычисления FPU, который заканчивается jmp
вместо ret
(ret
модифицирует rsp
).)
Но код ядра обычно избегает FPU, поэтому Linux оставляет состояние FPU несохраненным при входе из системного вызова, сохраняя его только до фактического переключения контекста на другой процесс пользовательского пространства или на kernel_fpu_begin
, В противном случае обычно возвращаются к тому же процессу в пользовательском пространстве на том же ядре, поэтому состояние FPU не нужно восстанавливать, потому что ядро его не трогало. (И именно здесь может произойти повреждение, если задача ядра действительно изменила состояние FPU. Я думаю, что это происходит в обоих направлениях: пространство пользователя также может повредить ваше состояние FPU).
Целочисленное состояние довольно мало, всего 16x 64-битных регистров + RFLAGS и сегментные регистры. Состояние FPU более чем в два раза больше даже без AVX: 8x 80-битных регистров x87 и 16x регистров XMM или YMM, или 32x ZMM (+ MXCSR и x87 status + управляющие слова). Также MPX bnd0-4
регистры объединены с "FPU". На этом этапе "состояние FPU" означает просто все нецелочисленные регистры. На моем Скайлэйке, dmesg
говорится x86/fpu: Enabled xstate features 0x1f, context size is 960 bytes, using 'compacted' format.
Посмотрите Понимание использования FPU в ядре Linux; современный Linux не делает переключений контекста с отложенным FPU по умолчанию для переключений контекста (только для переходов ядро / пользователь). (Но эта статья объясняет, что такое Lazy.)
Большинство процессов используют SSE для копирования / обнуления небольших блоков памяти в сгенерированном компилятором коде, а большинство реализаций строки /memcpy/memset библиотеки используют SSE/SSE2. Кроме того, теперь поддерживается аппаратное обеспечение оптимизированного сохранения / восстановления (xsaveopt
/ xrstor), поэтому "энергичное" сохранение / восстановление FPU может фактически выполнять меньше работы, если некоторые / все регистры FP фактически не использовались. например, сохранить только низкие 128b регистров YMM, если они были обнулены vzeroupper
поэтому процессор знает, что они чистые. (И отметьте этот факт одним битом в формате сохранения.)
При "активном" переключении контекста инструкции FPU остаются включенными все время, поэтому плохой код ядра может повредить их в любое время.
Не делай этого!
В пространстве ядра режим FPU отключен по нескольким причинам:
- Это позволяет Linux работать в архитектурах, которые не имеют FPU
- Это позволяет избежать сохранения и восстановления всего набора регистров при каждом переходе ядро / пользовательское пространство (это может удвоить время переключения контекста)
- В основном все функции ядра используют целые числа также для представления десятичных чисел -> вам, вероятно, не нужна плавающая точка
- В Linux вытеснение отключено, когда пространство ядра работает в режиме FPU
- Числа с плавающей точкой являются злом и могут привести к очень плохому неожиданному поведению
Если вы действительно хотите использовать номера FP (а не должны), вы должны использовать kernel_fpu_begin
а также kernel_fpu_end
примитивы, чтобы избежать взлома регистров пользовательского пространства, и вы должны принять во внимание все возможные проблемы (включая безопасность) при работе с числами FP.
Не уверен, откуда исходит это восприятие. Но ядро выполняется на том же процессоре, что и код пользовательского режима, и поэтому имеет доступ к тому же набору команд. Если процессор может выполнять операции с плавающей запятой (напрямую или сопроцессором), то и ядро тоже может.
Возможно, вы думаете о случаях, когда арифметика с плавающей запятой эмулируется в программном обеспечении. Но даже в этом случае оно будет доступно в ядре (ну, если не отключено каким-либо образом).
Мне любопытно, откуда это восприятие? Может быть, я что-то упустил.
Нашел это. Кажется, это хорошее объяснение.
Ядро ОС может просто выключить FPU в режиме ядра.
Во время работы FPU, во время работы с плавающей запятой ядро включит FPU, а после этого выключит FPU.
Но вы не можете распечатать его.