Начальное состояние программных регистров и стека в Linux ARM - PullRequest
6 голосов
/ 26 ноября 2009

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

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

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

Любые указатели приветствуются.

Ответы [ 4 ]

6 голосов
/ 14 мая 2011

Поскольку это 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 должен быть равен единице, если вы вызвали программу без аргументов)? Я предполагаю, что что-то глубоко в механизме системного вызова переписало это с возвращаемым значением системного вызова (которое будет равно нулю, так как exec преуспел). В kernel_execve (который не использует механизм системного вызова, поскольку ядро ​​вызывает его, когда он хочет выполнить из режима ядра) вы можете видеть, что он намеренно перезаписывает r0 возвращаемым значением do_execve.

3 голосов
/ 26 ноября 2009

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

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

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

2 голосов
/ 26 ноября 2009

Вот что я использую для запуска программы 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's go!
"       b       .\n"                    // Never gets here.
"       .size   _start, .-_start\n"
);

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

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

0 голосов
/ 26 ноября 2009

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

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

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

Tony

...