При загрузке программы Linux выделяет программе некоторую память в виде стека, но к моменту исполнения первой инструкции программы этот стек не пуст и уже сожержит ряд вспомогательной информации, связанной с текущей программой и системой. При загрузке программы стек начинается со следующих значений (от старших адресов к младшим):
------------------------------------------------------------ Нулевой указатель ------------------------------------------------------------ Указатель на n-ю переменную среды (при наличии) ------------------------------------------------------------ Указатель на вторую переменную среды (при наличии) ------------------------------------------------------------ Указатель на первую переменную среды (при наличии) ------------------------------------------------------------ Нулевой указатель ------------------------------------------------------------ Указатель на n-й аргумент программы (при наличии) ------------------------------------------------------------ Указатель на второй аргумент программы (при наличии) ------------------------------------------------------------ Указатель на первый аргумент программы (при наличии) ------------------------------------------------------------ Указатель на имя файла программы -------------------------------------------------------------- Количество аргументов командной строки ---------------------------------------------------------------
Таким образом, даже если мы находимся в точке входа _start
, указатель стека RSP содержит адрес, где располагается количество аргументов командной строки, с
которыми была вызвана программа. В это количество входит само название программы, поэтому, даже если аргументы не были введены, это значение будет равно 1.
Следующий 64-битное значение в стеке — это адрес текста с вызовом программы, с помощью которого был запущен исполняемый файл. Текст может быть полным, что означает, что имя пути включает путь к файлу из каталога /home, например, /home/asm/hello.
Если были введены какие-либо аргументы командной строки, их 64-битные адреса располагаются далее в стеке
Список адресов аргументов командной строки завершается нулевым указателем - числом 0.
Далее идут адреса строк, которые представляют переменные окружения.
Список адресов переменных окружения также завершается нулевым указателем.
Например, получим количество аргументов. Для этого определим следующую программу:
global _start section .text _start: mov rdi, [rsp] ; получим в RDI количество аргументов командной строки mov rax, 60 syscall
В данном случае просто помещаем количество аргументов в регистр RDI. Скомпилируем и запустим программу (в моем случае программа представляет файл "hello"):
root@Eugene:~/asm# ./hello root@Eugene:~/asm# echo $? 1 root@Eugene:~/asm#
Проверка кода возврата показала, что в RDI число 1 - это собственно и есть путь к файлу программы, который применялся для ее вызова - ./hello
.
Но передадим несколько аргументов:
root@Eugene:~/asm# ./hello arg1 arg2 arg3 root@Eugene:~/asm# echo $? 4 root@Eugene:~/asm#
При передаче трех аргументов мы видим, что возвращаемое число увеличилось до 4 - 3 аргумента + путь к файлу программы.
Теперь получим имя программы и выведем его на консоль:
global _start section .text _start: ; находим количество символов в строке mov rdi, [rsp+8] ; получим в RDI адрес пути к файлу, который применялся при вызове xor rax, rax ; AL равен 0 mov rcx, -1 ; помещаем очень большое число repne scasb ; ищем нулевой байт ; в rdi - адрес следующего за нулем символа mov byte [rdi-1], 10 ; заменяем нулевой байт переводом строки sub rdi, [rsp+8] ; получаем длину строки включая нулевой символ - RDI-[rsp+8] ; вывод на строки на экран mov rdx, rdi mov rdi, 1 mov rsi, [rsp+8] mov rax, 1 syscall mov rax, 60 syscall
Чтобы вевывети строку на консоль, нам нужно знать ее адрес и количество ее символов. С адресом все просто - это адрес, который хранится в [rsp+8]
. С количеством символов сложнее.
Строка по умолчанию завершается нулевым концевым байтом. Соответственно, чтобы определить количество символов, нам надо найти в строке позицию этого нулевого байта. Для этого в программе выше
применяем инструкцию repne scasb. Она перебирает строку в rdi, пока не найдет байт из регистра AL (куда мы помещаем 0, так как ищем нулевой байт) или пока значение в RCX не станет равным 0:
mov rdi, [rsp+8] ; получим в RDI адрес пути к файлу, который применялся при вызове xor rax, rax ; AL равен 0 mov rcx, -1 ; помещаем очень большое число repne scasb ; ищем нулевой байт
Поскольку пути к файлу могут быть длинными, их длина неизвестна, помещаем в rcx очень большое число, которое в дополнительном коде представляет число -1. После выполнения scasb регистр RDI будет содержать адрес следующего после нулевого байта символа. Поэтому если мы вычтем из адреса в RDI адрес начала строки, то как раз получим количество символов.
sub rdi, [rsp+8] ; получаем длину строки включая нулевой символ - RDI-[rsp+8]
Но перед этим заменяем нулевой символ на перевод строки, который имеет числовой код 10 - чтобы при выводе на консоль строка отделялась от других.
mov byte [rdi-1], 10 ; заменяем нулевой байт переводом строки
После этого применяется стандартный системный вызов номер 1 для вывода строки на консоль. Пример выполнения программы:
root@Eugene:~/asm# ./hello ./hello root@Eugene:~/asm#
То есть в моем случае выводится строка "./hello".
Строки аргументов также завершаются нулевым байтом. Поэтому применим подобную технику для получения их длины и вывода на консоль:
global _start section .bss section .text _start: mov rbx, 0 ; индекс первого аргумента mov r15, [rsp] ; в r15 помещаем количество аргументов scan_args: ; находим количество символов в строке mov rdi, [rsp+8+rbx*8] ; получим в RDI адрес аргумента с индексом RBX xor rax, rax ; AL равен 0 mov rcx, -1 ; помещаем очень большое число repne scasb ; ищем нулевой байт ; в rdi - адрес следующего за нулем символа mov byte [rdi-1], 10 ; заменяем нулевой байт переводом строки sub rdi, [rsp+8+rbx*8] ; получаем длину строки включая нулевой символ - RDI-[rsp+8] ; вывод на строки на экран mov rdx, rdi mov rdi, 1 mov rsi, [rsp+8+rbx*8] mov rax, 1 syscall inc rbx ; увеличиваем счетчик аргументов cmp rbx, r15 ; сравниваем с количеством аргументов jne scan_args mov rax, 60 syscall
В данном случае в регистр rbx помещаем индекс аргумента, а в регистр r15 - количество аргументов (включая имя программы). В цикле на метке scan_args походим по все аргументам, используя адрес [rsp+8+rbx*8]
,
получаем их длину и выводим на консоль.
Пример работы программы:
root@Eugene:~/asm# ./hello arg1 arg2 arg3 ./hello arg1 arg2 arg3 root@Eugene:~/asm#
Аналогично при необходимости можно получить и переменные окружения.