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
, но
генерятся хитро: они разделяются не по типу релокаций, которые надо
фиксить, а по простоте того, как их фиксить.
Например, обращение к глобальной переменной выглядит примерно так:
%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 | ...... ]
#
#
'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
!