Nikita Mandrykin

Практикуюсь в программировании. C • Python • Assembly • Linux

Назад

Аргументы командной строки в NASM: Работа с argc и argv

При разработке консольных утилит необходимо обрабатывать параметры запуска: имена файлов, флаги конфигурации и числовые значения. В отличие от 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... - маркер конца массива)  │
└──────────────────────┴──────────────────────────────────────────┘

Технические детали:

  1. Выравнивание: Все элементы массива занимают 8 байт (64 бита). argc также занимает 8 байт.
  2. Тип данных: Элементы argv являются указателями (адресами памяти), где хранятся null-терминированные строки.
  3. Содержимое: 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]) и выхода за границы массива.

✅ Заключение

Корректная работа с аргументами командной строки базируется на следующих принципах:

  1. Структура данных: Понимание расположения argc и массива указателей argv в стеке.
  2. Адресация: Использование правильных смещений (8 байт для 64-битной архитектуры).
  3. Валидация: Обязательная проверка argc перед доступом к элементам массива.
  4. Преобразование: Необходимость парсинга строковых данных в числовые типы для вычислений.

Эти механизмы являются стандартными для системного программирования в Linux и используются при создании любых утилит, принимающих параметры запуска.

💜

Полезный материал?

Если статья помогла разобраться в теме, вы можете поддержать автора любой комфортной суммой.

Поддержать через CloudTips