Начальное состояние программных регистров и стека в Linux ARM
Вопрос
В настоящее время я играю с ARM assembly в Linux в качестве учебного упражнения.Я использую "голую" сборку, т. е.нет libcrt или libgcc.Кто-нибудь может указать мне на информацию о том, в каком состоянии находятся указатель стека и другие регистры при запуске программы перед вызовом первой инструкции?Очевидно, что pc / r15 указывает на _start , а остальные, похоже, инициализированы в 0, за двумя исключениями;sp / r13 указывает на адрес, находящийся далеко за пределами моей программы, а r1 указывает на немного более высокий адрес.
Итак, к некоторым серьезным вопросам:
- Каково значение в r1?
- Является ли значение в sp законным стеком, выделяемым ядром?
- Если нет, то каков предпочтительный метод выделения стека;использовать brk или выделить статический раздел .bss?
Любые указания были бы оценены.
Решение
Вот что я использую, чтобы запустить программу 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 и environ из стека в [sp].
Небольшое уточнение:Указатель стека указывает на допустимую область в памяти процесса.r0, r1, r2 и r3 - это первые три параметра вызываемой функции.Я заполняю их с помощью argc, argv и environ соответственно.
Другие советы
Поскольку это 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
должен быть один, если вы вызвали программу без аргументов)?Я предполагаю, что что-то глубоко в механизме системного вызова переписало его возвращаемым значением системного вызова (которое было бы равно нулю с момента успешного выполнения).Вы можете видеть в kernel_execve
(который не использует механизм системного вызова, поскольку это то, что вызывает ядро, когда оно хочет выполнить из режима ядра), который оно намеренно перезаписывает r0
с возвращаемым значением do_execve
.
Вот этот ЭЛТ uClibc.Это, по-видимому, предполагает, что все регистры не определены, за исключением r0
(который содержит указатель на функцию, который должен быть зарегистрирован с atexit()
) и sp
который содержит действительный адрес стека.
Итак, значение, которое вы видите в r1
вероятно, это не то, на что вы можете положиться.
Некоторые данные помещаются в стек для вас.
Я никогда не использовал ARM Linux, но я предлагаю вам либо взглянуть на исходный код libcrt и посмотреть, что они делают, либо использовать gdb для перехода к существующему исполняемому файлу.Вам не должен понадобиться исходный код, просто перейдите к ассемблерному коду.
Все, что вам нужно выяснить, должно произойти в самом первом коде, выполняемом любым двоичным исполняемым файлом.
Надеюсь, это поможет.
Тони