2009-11-26 1 views
5

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

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

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

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

ответ

2

Вот что я использую, чтобы получить программу 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, и окружат вещи из стека в [SP] ,

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

+0

Спасибо. Описывается ли эта установка в любом месте, о котором вы знаете? –

+0

Я уверен, что это должно быть, но я должен признать, что я понял это с помощью gdb. –

0

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

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

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

Tony

3

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

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

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

+0

Полезная ссылка, спасибо. –

5

Поскольку это 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. Ниже находится в стеке в порядке, вы читали его:

  • число параметров (это в main()argc).
  • Один указатель на строку 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.