При разработке консольных утилит необходимо обрабатывать параметры запуска: имена файлов, флаги конфигурации и числовые значения. В отличие от C, где точка входа main предоставляет типизированные аргументы, в ассемблере взаимодействие происходит напрямую с «сырым» стеком, инициализированным ядром операционной системы.
В этой статье рассматривается структура стека при запуске процесса, методы извлечения данных и преобразование строковых аргументов.
🧠 1. Анатомия стека при запуске
При передаче управления точке входа _start, стек процесса имеет строго определенную структуру (System V ABI). Указатель стека RSP указывает на начало этой структуры:
Адреса (RSP растет вниз)
│
▼
┌──────────────────────┬──────────────────────────────────────────┐
│ [RSP] │ argc (Количество аргументов, 8 байт) │
├──────────────────────┼──────────────────────────────────────────┤
│ [RSP + 8] │ argv[0] (Указатель на имя программы) │
├──────────────────────┼──────────────────────────────────────────┤
│ [RSP + 16] │ argv[1] (Указатель на 1-й аргумент) │
├──────────────────────┼──────────────────────────────────────────┤
│ [RSP + 24] │ argv[2] (Указатель на 2-й аргумент) │
├──────────────────────┼──────────────────────────────────────────┤
│ ... │ ... │
├──────────────────────┼──────────────────────────────────────────┤
│ [RSP + 8*(argc+1)] │ NULL (0x0000... - маркер конца массива) │
└──────────────────────┴──────────────────────────────────────────┘Технические детали:
- Выравнивание: Все элементы массива занимают 8 байт (64 бита).
argcтакже занимает 8 байт. - Тип данных: Элементы
argvявляются указателями (адресами памяти), где хранятся null-терминированные строки. - Содержимое:
argv[0]содержит путь к исполняемому файлу. Реальные аргументы пользователя начинаются сargv[1].
🛠️ 2. Методы доступа к аргументам
Существует два основных подхода к чтению аргументов, выбор зависит от требований к сохранению состояния стека.
Способ А: Деструктивный (через pop)
Последовательное извлечение данных с модификацией регистра RSP. Подходит для линейной обработки аргументов при инициализации.
_start:
pop rcx ; RCX = argc
pop rdi ; RDI = argv[0] (имя программы)
dec rcx ; Уменьшаем счетчик (имя программы обработано)
jz .no_args ; Если 0 -> аргументов нет
pop rsi ; RSI = argv[1] (первый аргумент)
Способ Б: Индексный (через mov)
Чтение стека как массива через смещения относительно RSP. Сохраняет структуру стека неизменной, позволяет произвольный доступ.
_start:
mov rcx, [rsp] ; Чтение argc без изменения RSP
cmp rcx, 2 ; Проверка: argc >= 2 (программа + 1 аргумент)
jl .usage_error
mov rsi, [rsp + 16] ; argv[1] находится со смещением 16 байт
; (0=argc, 8=argv[0], 16=argv[1])
📝 3. Практика: Итерация по аргументам
Пример программы, выводящей все переданные аргументы построчно в стандартный поток вывода.
section .data
newline db 10
section .text
global _start
_start:
; --- Инициализация ---
pop r12 ; R12 = argc (счётчик цикла)
; pop сдвигает RSP на argv[0]
.loop:
test r12, r12 ; Проверка счётчика
jz .exit ; Если 0, выход
pop rsi ; RSI = текущий указатель argv[i]
call print_string ; Вывод строки
dec r12 ; Декремент счётчика
jmp .loop
.exit:
mov rax, 60 ; sys_exit
xor rdi, rdi ; код возврата 0
syscall
; ---------------------------------------------------------
; Функция вывода null-терминированной строки и LF
; Вход: RSI - адрес строки
; ---------------------------------------------------------
print_string:
push r12 ; Сохранение callee-saved регистров
push rsi ; Сохранение адреса начала
; 1. Вычисление длины (strlen)
xor rdx, rdx ; RDX = длина
.count:
cmp byte [rsi + rdx], 0
je .do_print
inc rdx
jmp .count
.do_print:
; 2. Вывод строки
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
pop rsi ; Восстановление адреса
syscall ; RDX уже содержит длину
; 3. Вывод перевода строки
mov rax, 1
mov rdi, 1
push 10 ; Помещение '\n' в стек
mov rsi, rsp ; Адрес символа = вершина стека
mov rdx, 1
syscall
pop rax ; Очистка стека
pop r12
ret
Пример вызова:
./echo hello world 123
# Вывод:
# ./echo
# hello
# world
# 123
🧮 4. Парсинг числовых аргументов
Аргументы командной строки всегда передаются как строки. Для выполнения арифметических операций необходимо преобразование типов (String to Int).
Рекомендация: Для production-кода следует использовать оптимизированные модули ввода-вывода (например,
io_signed.asmиз статьи «Модули ввода-вывода»). Ниже представлена базовая реализация алгоритмаatoi.
Задача: Сложить два числа, переданных аргументами.
section .data
msg_res db "Result: ", 0
newline db 10
section .text
global _start
_start:
; 1. Проверка количества аргументов
; Стек: [argc] [argv0] [argv1] [argv2]
cmp qword [rsp], 3
jl .exit_err ; Требуется минимум 3 элемента
; 2. Извлечение аргументов
mov rsi, [rsp + 16] ; argv[1]
call simple_atoi ; Преобразование в число
mov r12, rax ; Сохранение первого числа
mov rsi, [rsp + 24] ; argv[2]
call simple_atoi ; Преобразование в число
; 3. Арифметика
add rax, r12 ; Сумма
; 4. Возврат результата как кода выхода (для демонстрации)
mov rdi, rax
mov rax, 60
syscall
.exit_err:
mov rdi, 1 ; Код ошибки
mov rax, 60
syscall
; --- Базовый String to Int (беззнаковый) ---
; Вход: RSI - адрес строки
; Выход: RAX - число
simple_atoi:
xor rax, rax ; Аккумулятор
xor rcx, rcx ; Буфер цифры
.next_digit:
movzx rcx, byte [rsi] ; Чтение байта
test cl, cl ; Проверка на null-терминатор
jz .done
sub cl, '0' ; ASCII -> int
imul rax, 10 ; Сдвиг разряда
add rax, rcx ; Добавление цифры
inc rsi
jmp .next_digit
.done:
ret
Проверка кода возврата:
nasm -f elf64 sum.asm -o sum.o && ld sum.o -o sum
./sum 10 25
echo $?
# Вывод: 35
🐛 5. Инспекция стека в GDB
Для диагностики проблем с адресацией и содержимым регистров необходимо инспектировать стек напрямую в отладчике.
Передача аргументов
В GDB аргументы указываются команде run.
$ gdb ./args
(gdb) break _start
(gdb) run hello world 123 <-- Аргументы запуска
Анализ памяти стека
При остановке на _start, регистр rsp указывает на начало структуры аргументов.
# 1. Просмотр argc
(gdb) x/d $rsp
0x7fffffffdbf0: 4 # argc = 4
# 2. Просмотр массива argv (формат giant hex)
(gdb) x/4gx $rsp + 8
0x7fffffffd8f8: 0x00007fffffffdd31 0x00007fffffffdd54
0x7fffffffd908: 0x00007fffffffdd5a 0x00007fffffffdd60
Интерпретация вывода: GDB группирует по два 64-битных значения в строке:
...dd31(верхний левый) — argv[0]...dd54(верхний правый) — argv[1]...dd5a(нижний левый) — argv[2]...dd60(нижний правый) — argv[3]
Разыменование указателей
Просмотр содержимого строк по полученным адресам (используйте адреса из вашего вывода):
# argv[1] ("hello")
(gdb) x/s 0x00007fffffffdd54
0x7fffffffdd54: "hello"
# argv[2] ("world")
(gdb) x/s 0x00007fffffffdd5a
0x7fffffffdd5a: "world"
Диагностика через x/s позволяет выявить ошибки смещения (например, чтение argv[i] вместо argv[i+1]) и выхода за границы массива.
✅ Заключение
Корректная работа с аргументами командной строки базируется на следующих принципах:
- Структура данных: Понимание расположения
argcи массива указателейargvв стеке. - Адресация: Использование правильных смещений (8 байт для 64-битной архитектуры).
- Валидация: Обязательная проверка
argcперед доступом к элементам массива. - Преобразование: Необходимость парсинга строковых данных в числовые типы для вычислений.
Эти механизмы являются стандартными для системного программирования в Linux и используются при создании любых утилит, принимающих параметры запуска.