sparc relocations
Заметил я как-то однажды в lkml
письмо от Rob Landley (больше
всего знаменитого авторством busybox).
В письме описана проблема загрузки sparc32 ядра linux в
qemu:
Boot time fixup v1.6. 4/Mar/98 Jakub Jelinek (jj@ultra.linux.cz).
Patching kernel for srmmu[Fujitsu TurboSparc]/iommu
Fixup i f029ddfc doesn't refer to a valid instruction at
f00de648[95eea000]
halt, power off
Выглядело непонятно и до ужаса интересно. К письму даже прилагается работавшее ядро 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, но
генерятся хитро: они разделяются не по типу релокаций, которые надо
фиксить, а по простоте того, как их фиксить.
Например, обращение к глобальной переменной выглядит примерно так:
sethi %hi(bar), %g1 ! 1
ld [%g1+%lo(bar)], %o0 ! 2Грузится старших 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 */
set_addr(addr, q[1], fmangled, (insn & 0xffc00000) | (p[1] >> 10));
else if ((insn & 0x80002000) == 0x80002000 &&
(insn & 0x01800000) != 0x01800000) /* %LO */
set_addr(addr, q[1], fmangled, (insn & 0xffffe000) | (p[1] & 0x3ff));
else {
prom_printf(insn_i, p, addr, insn);
prom_halt();
}
break;В insn хранится код инструкции по адресу настраиваемой релокации. В
p[1] хранится разрешенный адрес (куда указывает релокация).
Чтобы понять эту битовую кашу надо чуточку знать формат инструкций.
Открываем SPARCv9 ISA и видим, что все инструкции состоят из 32 бит,
а смысл этих битов определяется старшими двумя битами:
Общий формат:
.op .
[ b b | ...... ]
#
#
'SETHI imm22, rd' и подобные (1 регистр, большой immediate)
.op=00. dest-reg . op2 . imm22 .
[ 0 0 | r r r r r | x x x | .... ]
#
#
# куча остальных трёхоперандных инструкций:
.op=10
.op=11. rest-reg . op3 . reg-src-1 . i . reg-src-2
[ b b | 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 ]
Всё не так страшно.
Теперь становится очевидно, что (insn & 0xc1c00000) == 0x01000000)
вылавливает такую инструкцию:
op = 00(SETHI-alike)op3 = 001(точненькоSETHI,HI22релокация)
а (insn & 0x80002000) == 0x80002000) отлавливает все трёхадресные
signed-imm13 инструкции. На самом деле не важно какие, так как мы
точно знаем тип релокации (LO10) и то, что релоцировано, но зачем-то
вставлено еще одно ограничение: (insn & 0x01800000) != 0x01800000.
Судя по всему это какие-то инструкции с op3=11????. Чем они не
угодили автору - не ясно, но в нашем случае
Fixup i f029ddfc doesn't refer to a valid instruction at
f00de648[95eea000]
halt, power off
туда попадает обычная релокация для инструкции 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!