Дополнительные статьи

Стек, аргументы программы и переменные окружения в Linux

Последнее обновление: 17.10.2023

При загрузке программы 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#

Аналогично при необходимости можно получить и переменные окружения.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850