Исходное состояние программных регистров и стека в Linux ARM

В настоящее время я играю с сборкой ARM на Linux в качестве учебного упражнения. Я использую "голую" сборку, т.е. Libcrt или libgcc. Может ли кто-нибудь указать мне информацию о том, в каком штате указатель стека и другие регистры будут в начале программы до вызова первой команды? Очевидно, pc/r15 указывает на _start, а остальные, по-видимому, инициализируются 0, за двумя исключениями; sp/r13 указывает на адрес, расположенный далеко за пределами моей программы, а r1 указывает на несколько более высокий адрес.

Итак, для некоторых твердых вопросов:

  • Каково значение в r1?
  • Является ли значение в sp законным стеком, выделенным ядром?
  • Если нет, то какой предпочтительный метод распределения стека; используя brk или выделите статический раздел .bss?

Любые указатели будут оценены.

Ответ 1

Здесь то, что я использую для запуска программы Linux/ARM, начиналось с моего компилятора:

/** The initial entry point.
 */
asm(
"       .text\n"
"       .globl  _start\n"
"       .align  2\n"
"_start:\n"
"       sub     lr, lr, lr\n"           // Clear the link register.
"       ldr     r0, [sp]\n"             // Get argc...
"       add     r1, sp, #4\n"           // ... and argv ...
"       add     r2, r1, r0, LSL #2\n"   // ... and compute environ.
"       bl      _estart\n"              // Let go!
"       b       .\n"                    // Never gets here.
"       .size   _start, .-_start\n"
);

Как вы можете видеть, я просто получаю материал argc, argv и environment из стека в [sp].

Небольшое пояснение: указатель стека указывает на допустимую область в памяти процесса. r0, r1, r2 и r3 являются первыми тремя параметрами вызываемой функции. Я заполняю их argc, argv и environ соответственно.

Ответ 2

Так как это Linux, вы можете посмотреть, как он реализован ядром.

Регистры, по-видимому, устанавливаются вызовом start_thread в конце load_elf_binary (если вы используете современную систему Linux, она почти всегда будет использовать формат ELF). Для ARM регистры выглядят следующим образом:

r0 = first word in the stack
r1 = second word in the stack
r2 = third word in the stack
sp = address of the stack
pc = binary entry point
cpsr = endianess, thumb mode, and address limit set as needed

Очевидно, что у вас есть действительный стек. Я думаю, что значения r0 - r2 являются нежелательными, и вы должны читать все из стека (вы поймете, почему я думаю об этом позже). Теперь посмотрим, что находится в стеке. То, что вы прочитаете из стека, заполняется create_elf_tables.

Интересно отметить, что эта функция не зависит от архитектуры, поэтому одни и те же вещи (в основном) будут помещаться в стек на каждой архитектуре Linux на базе ELF. В стеке должно быть указано следующее:

  • Число параметров (это argc в main()).
  • Один указатель на строку C для каждого параметра, за которым следует ноль (это содержимое argv в main(); argv будет указывать на первый из этих указателей).
  • Один указатель на строку C для каждой переменной среды, за которой следует ноль (это содержимое редко наблюдаемого envp третьего параметра main(); envp будет указывать на первый из этих указателей).
  • "Вспомогательный вектор", который представляет собой последовательность пар (тип, за которым следует значение), заканчивается парой с нулем (AT_NULL) в первом элементе. Этот вспомогательный вектор содержит некоторую интересную и полезную информацию, которую вы можете увидеть (если вы используете glibc), запустив любую динамически связанную программу с переменной среды LD_SHOW_AUXV, установленной на 1 (например, LD_SHOW_AUXV=1 /bin/true). Это также может быть немного зависеть от архитектуры.

Поскольку эта структура одинакова для каждой архитектуры, вы можете посмотреть пример на чертеже на странице 54 SYSV 386 ABI, чтобы получить лучшее представление о том, как вещи сочетаются (обратите внимание, однако, что константы вспомогательного векторного типа в этом документе отличаются от того, что использует Linux, поэтому вы должны посмотреть на заголовки Linux для них).

Теперь вы можете понять, почему содержимое r0 - r2 является мусором. Первое слово в стеке argc, второе - указатель на имя программы (argv[0]), а третий, вероятно, был для вас нулевым, потому что вы вызывали программу без аргументов (это было бы argv[1]), Я думаю, они настроены таким образом для более старого a.out двоичного формата, который, как вы можете видеть на create_aout_tables, ставит argc, argv и envp в стеке (так что они попадут в r0 - r2 в порядке, ожидаемом для вызова main()).

Наконец, почему r0 ноль для вас вместо одного (argc должен быть одним, если вы вызываете программу без аргументов)? Я догадываюсь, что что-то глубоко в машине syscall переписало его с возвращаемым значением системного вызова (которое было бы нулевым с момента успешного выполнения exec). Вы можете видеть в kernel_execve (который не использует механизм syscall, поскольку это то, что ядро ​​вызывает, когда оно хочет выполнить из режима ядра), что он намеренно перезаписывает r0 с возвращаемым значением do_execve.

Ответ 3

Здесь uClibc crt. Кажется, что все регистры undefined кроме r0 (который содержит указатель на функцию, который должен быть зарегистрирован с atexit()) и sp, который содержит допустимый адрес стека.

Итак, значение, которое вы видите в r1, вероятно, не на что-то, на что вы можете положиться.

Некоторые данные помещаются в стек для вас.

Ответ 4

Я никогда не использовал ARM Linux, но я предлагаю вам либо взглянуть на источник libcrt и посмотреть, что он делает, либо использовать gdb для входа в существующий исполняемый файл. Вам не нужно, чтобы исходный код просто проходил через код сборки.

Все, что вам нужно выяснить, должно происходить в самом первом коде, исполняемом любым исполняемым файлом.

Надеюсь, что это поможет.

Тони