sparc relocations

December 19, 2011

Заметил я как-то однажды в 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 интересовался что за код настраивает релокации и кто их генерирует.

Код оказалось легко найти в ядре (более того, он простой как грабли):

Этого ему показалось достаточно и он ушёл.

Через 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 (судя по всему для настройки релокаций в модулях ядра).

Вообще релокации нужны для того, чтобы исполняемый файл (или динамическую библиотеку) можно было:

Первый случай отпадает сразу. Ядро 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) вылавливает такую инструкцию:

а (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!