sparc relocations
Заметил я как-то однажды в lkml письмо от Rob Landley (больше всего знаменитого авторством busybox).
В письме описана проблема загрузки sparc32 ядра linux в qemu:
.6. 4/Mar/98 Jakub Jelinek (jj@ultra.linux.cz).
Boot time fixup v1for srmmu[Fujitsu TurboSparc]/iommu
Patching kernel 't refer to a valid instruction at
Fixup i f029ddfc doesn[95eea000]
f00de648, power off halt
Выглядело непонятно и до ужаса интересно. К письму даже прилагается работавшее ядро 3.0 и минимальный юзерленд.
Чуть позже я забыл про это письмо. Но спустя пару недель на #gentoo-sparc:
20:05 -!- landley [~landley@140.242.26.2] has joined #gentoo-sparc
20:06 < landley> Ah, significantly more people in here than on #sparc, maybe somebody here can figure out http://lkml.org/lkml/2011/11/12/57
20:06 < landley> Sparc apparently has some kind of dynamic symbol relocator, and it barfs mightily on the ext4 code.
20:06 < landley> As in it tries to relocate one of those register wheel instructions.
20:08 < landley> I blogged about it at http://landley.net/notes.html#12-11-2011 but haven't made any progress fixing it since...
Я понял, что мне не отвертеться и придется корчить из себя гентушника-спарковода. Rob интересовался что за код настраивает релокации и кто их генерирует.
Код оказалось легко найти в ядре (более того, он простой как грабли):
- arch/sparc/boot/btfixupprep.c - генератор таблицы релокаций
- arch/sparc/mm/btfixup.c - стартовый код, который и настраивает сразу после распаковки образа
Этого ему показалось достаточно и он ушёл.
Через 2 недели после этого в lkml ничего нового не появилось и я попробовал воспроизвести баг. После вытресания .config баг проявился во всей красе.
Немного теории:
linux ядро собирается gcc в ELF файл с заголовками, секциями и прочей фигнёй (это естественный формат вывода для gcc и ld на linux). Такое ядро называется vmlinux.
grub и прочие загрузчики обычно слишком тупы, чтобы грузить vmlinux прямо с диска. Загрузчики обычно просто считывают весь файл в память и передают ему управление на первую инструкцию файла. Стартовый код должен сам выискивать точку входа в нашем vmlinux предварительно донастраивая аппаратуру и ELF ошмётки (о них ниже).
Этот механизм (вставки произвольного стартового кода) позволяет делать с оригинальным vmlinux что угодно. Например, оригинальный образ можно сжать архиватором, а в стартовый код поместить разархиватор. Такой сжатый образ с распаковщиком уже не является ELF файлом и называется vmlinuz.
После разархивации управление передается на код настройки железа (header.S для x86, head_32.S для sparc32), оттуда управление получает main() (arch/) и start_kernel() (init/main.c).
Некоторые архитектуры позволяют извернуться так, чтобы ядро вообще не надо было настраивать после распаковки в память. Распаковал по абсолютному адресу, заданному при компиляции -и голова не боли. С виду на x86 так и есть: _start (arch/x86/boot/header.S) передает управление в main() -> go_to_protected_mode() -> protected_mode_jump() (pmjump.S) -> start_kernel
В sparc управледние передается на prom_init() (arch/sparc/kernel/head_32.S) перед start_kernel(). В start_kernel() уже вызывается btfixup(), которая настраивает релокации. Релокации не настроились на стадии финальной сборки vmlinux потому что они генерятся/используются явно в arch/sparc/include/asm/btfixup.h (судя по всему для настройки релокаций в модулях ядра).
Вообще релокации нужны для того, чтобы исполняемый файл (или динамическую библиотеку) можно было:
- настроить внешние ссылки (ссылки на символы из других модулей, например, функций из libc)
- загрузить по адресу, отличному от того, для которого его изначально предполагал размещать ld на стадии компиляции
Первый случай отпадает сразу. Ядро linux полностью самодостаточно и ссылается только на себя. Второй случай возможен (хоть и не является типичным). Его легко решить в частном случае. Нам нужно знать только смещение от огригинального адреса загрузки и хранить где-то список всех мест, куда надо вписать новый адрес.
Эти смещени яи генерятся прогой arch/sparc/boot/btfixupprep.c, но генерятся хитро: они разделяются не по типу релокаций, которые надо фиксить, а по простоте того, как их фиксить.
Например, обращение к глобальной переменной выглядит примерно так:
%hi(bar), %g1 ! 1
sethi [%g1+%lo(bar)], %o0 ! 2 ld
Грузится старших 22 бита (1: релокация R_SPARC_HI22) в регистр %g1, потом складывается с младшими 10 (2: релокация R_SPARC_LO10).
Итого надо патчить 2 инструкции, в которых закодированы все 32 бита абсолютного адреса переменной bar. Вместо того, чтобы хранить разные типы релокаций отдельно (ну впримере их две: HI22 и LO10) аффторы решили замутить чудоэвристику: если инструкция SETHI - значит HI22 релокация, иначе (но не всегда) - LO10.
Посмотрим теперь на настройку релокаций (arch/sparc/mm/btfixup.c:btfixup()):
....
case 'i': /* INT */
if ((insn & 0xc1c00000) == 0x01000000) /* %HI */
(addr, q[1], fmangled, (insn & 0xffc00000) | (p[1] >> 10));
set_addrelse if ((insn & 0x80002000) == 0x80002000 &&
(insn & 0x01800000) != 0x01800000) /* %LO */
(addr, q[1], fmangled, (insn & 0xffffe000) | (p[1] & 0x3ff));
set_addrelse {
(insn_i, p, addr, insn);
prom_printf();
prom_halt}
break;
В insn хранится код инструкции по адресу настраиваемой релокации. В p[1] хранится разрешенный адрес (куда указывает релокация)
Чтобы понять эту битовую кашу надо чуточку знать формат инструкций. Открываем SPARCv9 ISA и видим, что все инструкции состоят из 32бит, а смысл этих битов определяется старшими двумя битами:
Общий формат:.
.op | ...... ]
[ b b
#
#, rd' и подобные (1 регистр, большой immediate)
'SETHI imm22=00. dest-reg . op2 . imm22 .
.op| x x x | .... ]
[ 0 0 | r r r r r
#
#
# куча остальных трёхоперандных инструкций:=10
.op=11. rest-reg . op3 . reg-src-1 . i . reg-src-2
.op| r r r r r | o o o o o o | r r r r r | 0 | ???????? r r r r r ]
[ b b | r r r r r | o o o o o o | r r r r r | 1 | signed-imm13 ] [ b b
Всё не так страшно :]
Теперь становится очевидно, что (insn & 0xc1c00000) == 0x01000000) вылавливает такую инструкцию:
- op = 00 (SETHI-alike)
- op3 = 001 (точненько SETHI, HI22 релокация)
а (insn & 0x80002000) == 0x80002000) отлавливает все трёхадресные signed-imm13 инструкции. На самом деле не важно какие, так как мы точно знаем тип релокации (LO10) и то, что релоцировано, но зачем-то вставлено еще одно ограничение: (insn & 0x01800000) != 0x01800000.
Судя по всему это какие-то инструкции с op3=11????. Чем они не угодили автору - не ясно, но в нашем случае
't refer to a valid instruction at
Fixup i f029ddfc doesn[95eea000]
f00de648, power off halt
туда попадает обычная релокация для инструкции 0x95eea000 (инструкция RESTORE, tail call).
Фикс простой как грабли:
--- a/arch/sparc/mm/btfixup.c
+++ b/arch/sparc/mm/btfixup.c
@@ -302,8 +302,7 @@ void __init btfixup(void)
case 'i': /* INT */
if ((insn & 0xc1c00000) == 0x01000000) /* %HI */
set_addr(addr, q[1], fmangled, (insn & 0xffc00000) | (p[1] >> 10));- else if ((insn & 0x80002000) == 0x80002000 &&
- (insn & 0x01800000) != 0x01800000) /* %LO */
+ else if ((insn & 0x80002000) == 0x80002000) /* %LO */
set_addr(addr, q[1], fmangled, (insn & 0xffffe000) | (p[1] & 0x3ff));
else { prom_printf(insn_i, p, addr, insn);
После этого ядро радостно грузится в qemu!