Nikita Mandrykin

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

Назад

Отладка ASM в VS Code: Настройка GDB и визуальный интерфейс

📚 Содержание

При разработке программ полностью на ассемблере NASM с меткой _start возникает вопрос: как это удобно отлаживать? Хочется использовать мощный графический инструмент VS Code с breakpoints, step-by-step выполнением и просмотром переменных.

🚀 1. Быстрый старт

Если нужно быстро начать, вот минимальный набор действий:

Для простых программ (без argc/argv)

1. Создайте src/debug_wrapper.c:

extern void asm_main(void);

int main(void) {
    asm_main();
    return 0;
}

2. В вашем .asm файле замените:

global _start
_start:
на:
global asm_main
asm_main:

3. Измените Makefile:

build_signed: prepare_dirs
+	gcc -c -g -ggdb -o build/debug_wrapper.o src/debug_wrapper.c
 	nasm -f elf64 -g -F dwarf src/io_signed.asm -o build/io_signed.o
 	nasm -f elf64 -g -F dwarf src/signed_a.asm -o build/signed_a.o
-	ld -m elf_x86_64 -o bin/signed build/signed_a.o build/io_signed.o
+	gcc -o bin/signed build/debug_wrapper.o build/signed_a.o build/io_signed.o -m64 -no-pie -z noexecstack

4. Запустите отладку: поставьте breakpoint в debug_wrapper.c на строке asm_main(); и нажмите F5.

Для программ с параметрами командной строки

Потребуется дополнительно адаптировать код для получения argc/argv через регистры вместо стека. Подробности в разделе “Специальная адаптация для программ с аргументами”.

💡 2. Зачем это нужно?

В чём корень проблемы?

Для VS Code не существует полнофункционального расширения, которое бы реализовывало Debug Adapter Protocol (DAP) специально для ассемблера NASM.

Debug Adapter Protocol (DAP) — это стандартный протокол, который позволяет интерфейсу VS Code “общаться” с реальными отладчиками (такими как GDB).

Поскольку нативного “Assembly-адаптера” не существует, здесь используется хитрость: расширение C/C++ от Microsoft. Оно умеет работать с GDB, но оптимизировано под стандартный C/C++ проект. Наша “чистая” ассемблерная программа с меткой _start, слинкованная через ld, не соответствует этой модели — отсюда и проблемы.

Что мы получим?

Графический отладчик — визуальный интерфейс вместо консольного GDB
Breakpoints — остановка выполнения на любой строке
Step-by-step выполнение — пошаговая трассировка кода
Просмотр переменных — значения регистров и памяти в реальном времени
Call Stack — цепочка вызовов функций
Watch expressions — отслеживание произвольных выражений

Для кого это актуально?

Этот метод особенно полезен при выполнении:

  • Лабораторных работ №3, 4 и 5 по ассемблеру
  • Собственных проектов на чистом ассемблере, например, с использованием готовых модулей ввода-вывода
  • Системного программирования с прямыми системными вызовами
  • Низкоуровневой разработки без использования стандартной библиотеки C

🤔 3. Какой подход выбрать?

Существует два основных способа отладки ассемблерного кода в VS Code:

Подход Преимущества Недостатки Когда использовать
Консольный GDB Не требует изменения кода Нет графического интерфейса Простая отладка, опытные пользователи
Графический отладчик Удобный интерфейс, breakpoints Требует временные изменения Сложная отладка, визуальное изучение

В этой статье рассматривается графический подход, так как он даёт максимальное удобство для обучения и разработки. Подробнее о консольном GDB можно узнать в статье «Настройка NASM x86-64 и VS Code: Гайд для Linux и Windows».

🔧 4. Как это работает?

Архитектура решения

┌─────────────────────────────────────────────────────────┐
│                   VS Code (интерфейс)                   │
├─────────────────────────────────────────────────────────┤
│              C/C++ Extension (DAP адаптер)              │
├─────────────────────────────────────────────────────────┤
│                    GDB (отладчик)                       │
├─────────────────────────────────────────────────────────┤
│   ┌──────────────────┐      ┌────────────────────┐      │
│   │ debug_wrapper.c  │ ───▶ │  asm_main() в .asm │      │
│   │  (точка входа)   │      │  (ваш код)         │      │
│   └──────────────────┘      └────────────────────┘      │
└─────────────────────────────────────────────────────────┘

Ключевая идея

💡 Решение: временно добавляем минимальную C-обёртку, которая вызывает нашу ассемблерную функцию. Программа при этом всё равно завершается через ассемблерный код — C нужен только для старта отладчика. Для финальной версии обёртка удаляется.

Важно понимать:

  1. C-обёртка НЕ управляет выполнением программы
  2. Она только запускает вашу ассемблерную функцию
  3. Программа завершается через syscall в ассемблерном коде
  4. Весь основной код остаётся полностью на ассемблере

Два типа программ

Процесс настройки зависит от того, использует ли ваша программа параметры командной строки:

Простые программы

Без параметров командной строки

  • Только замена _start на asm_main
  • Весь остальной код остаётся без изменений
  • Минимальные временные изменения

Примеры:

Консольный ввод/вывод, простые вычисления, лабораторные работы №3 и №5

С параметрами

С argc/argv

  • Замена _start на asm_main
  • Адаптация получения параметров
  • Аргументы передаются через регистры вместо стека

Примеры:

Обработка файлов, лабораторная №4

📝 5. Практика: Пошаговая настройка

Предварительные требования

Установите необходимые инструменты. Подробное пошаговое руководство по настройке окружения в Windows (WSL) и Linux доступно в статье «Настройка NASM x86-64 и VS Code: Гайд для Linux и Windows».

Краткий список:

# Установка всего необходимого
sudo apt update && sudo apt install build-essential gdb nasm -y

# Проверка установки
gcc --version
nasm --version
gdb --version

Расширения VS Code:

Шаг 1: Конфигурация VS Code

Создайте в корне проекта папку .vscode с двумя файлами конфигурации.

Файл .vscode/tasks.json

Автоматизирует сборку проекта:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Make all",
      "type": "shell",
      "command": "make",
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": ["$gcc"]
    },
    {
      "label": "Make clean",
      "type": "shell",
      "command": "make clean"
    }
  ]
}

Файл .vscode/launch.json

Вариант 1: Программы без аргументов

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "🔍 Debug",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/bin/main",
      "args": [],
      "stopAtEntry": false,
      "cwd": "${workspaceFolder}",
      "environment": [],
      "externalConsole": false,
      "MIMode": "gdb",
      "miDebuggerPath": "/usr/bin/gdb",
      "preLaunchTask": "Make all",
      "setupCommands": [
        {
          "description": "Включить pretty-printing для gdb",
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        },
        {
          "description": "Установить Intel-синтаксис",
          "text": "set disassembly-flavor intel",
          "ignoreFailures": true
        }
      ]
    }
  ]
}

Вариант 2: Программы с аргументами командной строки

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "🔍 Debug (только файл)",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/bin/main",
      "args": ["data/input.txt"],
      "stopAtEntry": false,
      "cwd": "${workspaceFolder}",
      "externalConsole": false,
      "MIMode": "gdb",
      "miDebuggerPath": "/usr/bin/gdb",
      "preLaunchTask": "Make all",
      "setupCommands": [
        {
          "description": "Включить pretty-printing",
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        },
        {
          "description": "Intel-синтаксис",
          "text": "set disassembly-flavor intel",
          "ignoreFailures": true
        }
      ]
    },
    {
      "name": "🔍 Debug (файл + параметры)",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/bin/main",
      "args": ["data/input.txt", "-100", "100", "5"],
      "stopAtEntry": false,
      "cwd": "${workspaceFolder}",
      "externalConsole": false,
      "MIMode": "gdb",
      "preLaunchTask": "Make all",
      "setupCommands": [
        {
          "description": "Включить pretty-printing",
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        },
        {
          "description": "Intel-синтаксис",
          "text": "set disassembly-flavor intel",
          "ignoreFailures": true
        }
      ]
    }
  ]
}

💡 Настройте под свой проект: Измените значения в массиве args и путь к исполняемому файлу в program в соответствии с вашими требованиями.

Шаг 2: Создайте C-обёртку

Создайте файл src/debug_wrapper.c:

Для программ без аргументов
// src/debug_wrapper.c
extern void asm_main(void);

int main(void) {
    asm_main();
    // Программа завершится внутри asm_main через syscall
    return 0;  // Эта строка не выполнится
}
Для программ с argc/argv
// src/debug_wrapper.c
extern void asm_main(int argc, char** argv);

int main(int argc, char** argv) {
    asm_main(argc, argv);
    // Программа завершится внутри asm_main через syscall
    return 0;  // Эта строка не выполнится
}

⚠️ Важно: C-обёртка НЕ управляет завершением программы. Она только запускает вашу ассемблерную функцию, которая сама завершит выполнение через syscall.

Шаг 3: Измените точку входа в .asm файле

Для всех программ замените _start на asm_main:

❌ Было
section .text
    global _start

_start:
    ; === Ваш код ===
    call read_input
    call process_data
    call print_output
    
    mov rax, 60
    xor rdi, rdi
    syscall
✅ Стало
section .text
    global asm_main

asm_main:
    ; === Ваш код БЕЗ ИЗМЕНЕНИЙ ===
    call read_input
    call process_data
    call print_output
    
    mov rax, 60         ; sys_exit - оставляем!
    xor rdi, rdi
    syscall             ; Программа завершится здесь

🚨 Критически важно:

  • Меняется только global _startglobal asm_main и метка _start:asm_main:
  • syscall для выхода ОСТАЁТСЯ! Не заменяйте его на ret
  • Весь остальной код остаётся АБСОЛЮТНО без изменений

Шаг 4: Специальная адаптация для программ с аргументами

Если ваша программа использует аргументы командной строки (argc, argv), требуется дополнительное изменение: адаптация получения аргументов.

Почему это необходимо?

Это различие фундаментально и регулируется соглашением о вызовах System V ABI. Детальное описание ролей регистров (RDI, RSI, RDX и т.д.) можно найти в «Шпаргалка NASM x86-64: Регистры, Инструкции, Syscalls (Linux)».

Через _start (стек)
_start:
    pop rax        ; argc
    pop rbx        ; argv[0]
    pop rdi        ; argv[1]

При запуске программы через _start

  • Аргументы argc и argv лежат на стеке
  • Первое значение [rsp] = количество аргументов
  • Далее идут указатели:
    • [rsp+8] = argv[0]
    • [rsp+16] = argv[1]
    • [rsp+24] = argv[2]
    • и так далее…
Через asm_main (регистры)
asm_main:
    ; rdi = argc, rsi = argv
    mov r8, [rsi + 8]   ; argv[1]

При вызове asm_main(int argc, char** argv) из C

  • Аргументы передаются через регистры (согласно x86-64 System V ABI)
  • rdi = argc (количество аргументов)
  • rsi = argv (указатель на массив строк)
  • Доступ к элементам через смещения:
    • [rsi] = argv[0]
    • [rsi+8] = argv[1]
    • [rsi+16] = argv[2]

Визуальное сравнение

╔═══════════════════════════════════════════════════════════════╗
║                      ДОСТУП К АРГУМЕНТАМ                      ║
╠═══════════════════════════════════════════════════════════════╣
║                                                               ║
║  Через _start (стек):              Через asm_main (регистры): ║
║  ┌─────────────┐                   ┌─────────────┐            ║
║  │ [rsp] = 3   │ ← argc            │ rdi = 3     │ ← argc     ║
║  ├─────────────┤                   ├─────────────┤            ║
║  │ [rsp+8]     │ ← argv[0]         │ rsi = &argv │            ║
║  ├─────────────┤                   │             │            ║
║  │ [rsp+16]    │ ← argv[1]         │ [rsi+0]     │ ← argv[0]  ║
║  ├─────────────┤                   │ [rsi+8]     │ ← argv[1]  ║
║  │ [rsp+24]    │ ← argv[2]         │ [rsi+16]    │ ← argv[2]  ║
║  └─────────────┘                   └─────────────┘            ║
║                                                               ║
╚═══════════════════════════════════════════════════════════════╝

Пошаговая трансформация кода

ШАГ 1: Получение argc

❌ Было — получение argc со стека
_start:
    pop     rax          ; Извлекаем argc из стека
    cmp     rax, 2       ; Проверяем количество аргументов
    jl      .error_usage ; Если < 2, показываем usage
✅ Стало — argc уже в регистре
asm_main:
    ; argc УЖЕ в rdi, извлекать не нужно!
    cmp     rdi, 2       ; Проверяем количество аргументов
    jl      .error_usage ; Если < 2, показываем usage

ШАГ 2: Получение argv[1] (первый аргумент)

❌ Было — получение argv[0] и argv[1] со стека
_start:
    pop     rax      ; argc
    pop     rbx      ; argv[0] (пропускаем)
    pop     rdi      ; argv[1] (первый аргумент)
✅ Стало — доступ через массив argv
asm_main:
    ; rdi = argc
    ; rsi = argv
    
    ; Получаем argv[1] (первый аргумент)
    mov     rdi, [rsi + 8]

Таблица доступа к argv:

Элемент Смещение Код Описание
argc rdi Количество аргументов
argv rsi Указатель на массив
argv[0] 0 mov r8, [rsi] Имя программы
argv[1] 8 mov r9, [rsi + 8] Первый аргумент
argv[2] 16 mov r10, [rsi + 16] Второй аргумент
argv[3] 24 mov r11, [rsi + 24] Третий аргумент
argv[4] 32 mov r12, [rsi + 32] Четвёртый аргумент

Критически важно: Всегда сохраняйте rsi на стеке (push rsi) перед использованием других регистров, и восстанавливайте (pop rsi), когда снова нужен доступ к argv!

Полный пример трансформации

Развернуть полный пример для программы с файлом и параметрами
section .data
     usage_msg db "Usage: ./program <filename>", 10
     usage_len equ $ - usage_msg

 section .text
-    global _start
+    global asm_main

-_start:
+asm_main:
+    ; При входе:
+    ; rdi = argc
+    ; rsi = argv
+    
     ; Проверка argc
-    pop     rax                       ; Извлекаем argc из стека
-    cmp     rax, 2                    ; Проверяем количество аргументов
+    cmp     rdi, 2                    ; argc УЖЕ в rdi
     jl      .show_usage
     
-    ; Получение имени файла (argv[1])
-    pop     rbx                       ; argv[0] (имя программы) - пропускаем
-    pop     rdi                       ; argv[1] (имя файла) - сохраняем в rdi
+    ; Сохраняем rsi, т.к. rdi будем использовать для файла
+    push    rsi                       ; ДОБАВЛЕНО: сохраняем указатель на argv
+    
+    ; Получение имени файла (argv[1])
+    mov     rdi, [rsi + 8]            ; ИЗМЕНЕНО: доступ через массив
     
     ; Открытие файла
     mov     rax, 2                    ; sys_open
+    push    rsi                       ; ДОБАВЛЕНО: сохраняем rsi
     xor     rsi, rsi                  ; O_RDONLY
     xor     rdx, rdx
     syscall
+    pop     rsi                       ; ДОБАВЛЕНО: восстанавливаем rsi
     
     ; Проверка ошибки открытия
     test    rax, rax
     js      .error_open
     
     ; Далее работа с файлом...
     ; (этот код остаётся БЕЗ ИЗМЕНЕНИЙ)
     
     ; Завершение (БЕЗ ИЗМЕНЕНИЙ)
     mov     rax, 60
     xor     rdi, rdi
     syscall

 .show_usage:
     mov     rax, 1
     mov     rdi, 1
     mov     rsi, usage_msg
     mov     rdx, usage_len
     syscall
     
     mov     rax, 60
     mov     rdi, 1
     syscall

 .error_open:
     mov     rax, 60
     mov     rdi, 1
     syscall

Ключевые изменения

Аспект Было (_start) Стало (asm_main) Почему
Получение argc pop rax rdi (уже здесь) Передаётся через регистр
Проверка argc cmp rax, 2 cmp rdi, 2 argc теперь в rdi
Пропуск argv[0] pop rbx Не нужно Можем сразу обратиться к нужному элементу
Получение argv[1] pop rdi mov rdi, [rsi + 8] Доступ через массив
Сохранение rsi Не нужно push rsi / pop rsi rsi нужен для доступа к argv

Частые ошибки при адаптации

❌ Ошибка 1: Забыли сохранить rsi
❌ Неправильно
asm_main:
    mov     rdi, [rsi + 8]
    ; ... используем rdi для системного вызова ...
    mov     r8, [rsi + 16]    ; ❌ rsi мог быть перезаписан!
✅ Правильно
asm_main:
    push    rsi               ; Сохраняем rsi
    mov     rdi, [rsi + 8]
    ; ... используем rdi ...
    pop     rsi               ; Восстанавливаем rsi
    mov     r8, [rsi + 16]    ; ✅ Теперь безопасно
❌ Ошибка 2: Неправильное смещение
❌ Неправильно
mov     rdi, [rsi + 1]    ; ❌ Смещение 1 байт, а не 8!
✅ Правильно
mov     rdi, [rsi + 8]    ; ✅ Каждый указатель = 8 байт
❌ Ошибка 3: Попытка использовать pop
❌ Неправильно
asm_main:
    pop     rax           ; ❌ Стек не содержит argc!
✅ Правильно
asm_main:
    ; argc УЖЕ в rdi, ничего не нужно извлекать
    cmp     rdi, 2

Чек-лист адаптации для программ с аргументами

  • global _startglobal asm_main
  • _start:asm_main:
  • Удалить pop rax для argc → использовать rdi напрямую
  • Изменить cmp rax, Ncmp rdi, N
  • Удалить все pop ... для argv → использовать mov ..., [rsi + offset]
  • Добавить push rsi перед использованием других регистров
  • Добавить pop rsi когда снова нужен доступ к argv
  • Убедиться, что смещения кратны 8: [rsi + 8], [rsi + 16], [rsi + 24]
  • syscall для выхода остаётся без изменений!

Шаг 5: Модифицируйте Makefile

Изменения в Makefile идентичны для всех типов программ. Рассмотрим на примере стандартного проекта.

Предполагаемая структура проекта

Перед началом убедитесь, что структура вашего проекта выглядит так:

project/
├── src/
│   ├── debug_wrapper.c      # ← Создайте этот файл (Шаг 2)
│   └── main.asm             # Основная программа (измените _start → asm_main)
├── data/                    # Опционально: тестовые данные
│   └── test.txt
├── build/                   # Создаётся автоматически при сборке
├── bin/                     # Создаётся автоматически при сборке
└── Makefile                 # ← Модифицируйте этот файл (Шаг 5)

💡 Важно: Папки build/ и bin/ создаются автоматически при выполнении make (target prepare_dirs в Makefile).

Сравнение: Было → Стало

Финальная версия (для сдачи)
.PHONY: all prepare_dirs clean

all: prepare_dirs
	nasm -f elf64 -g -F dwarf src/main.asm -o build/main.o
	ld -m elf_x86_64 -o bin/main build/main.o

prepare_dirs:
	@mkdir -p build bin

clean:
	@echo "Cleaning project..."
	@rm -f build/*.o bin/*
	@echo "Done."
Версия для отладки
.PHONY: all prepare_dirs clean

all: prepare_dirs
	gcc -c -g -ggdb -o build/debug_wrapper.o src/debug_wrapper.c
	nasm -f elf64 -g -F dwarf src/main.asm -o build/main.o
	gcc -g -o bin/main build/debug_wrapper.o build/main.o -no-pie

prepare_dirs:
	@mkdir -p build bin

clean:
	@echo "Cleaning project..."
	@rm -f build/*.o bin/*
	@echo "Done."

Что изменилось (построчно)

1. Добавлена компиляция C-обёртки:

gcc -c -g -ggdb -o build/debug_wrapper.o src/debug_wrapper.c
Флаг Назначение
-c Компилировать без линковки (создать .o файл)
-g Включить отладочную информацию
-ggdb Генерировать отладочную информацию в формате GDB

2. Линковка изменена с ld на gcc:

❌ Было
ld -m elf_x86_64 -o bin/main build/main.o
✅ Стало
gcc -g -o bin/main build/debug_wrapper.o build/main.o -no-pie

Разбор изменений:

Изменение Зачем
ldgcc GCC автоматически подключает C runtime и стандартные библиотеки
build/debug_wrapper.o первым C runtime ищет функцию main() в первом объектном файле
Добавлен -g Сохраняет отладочную информацию при линковке
Добавлен -no-pie Отключает Position Independent Executable — делает адреса предсказуемыми для отладчика
Удалён -m elf_x86_64 Этот флаг нужен только для ld, GCC определяет архитектуру автоматически

💡 Почему debug_wrapper.o должен быть первым?

При линковке GCC ищет точку входа main() в первом объектном файле. Если поставить main.o первым, линковщик не найдёт main() и выдаст ошибку:

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000

Адаптация для разных случаев

Пример для многомодульного проекта:

all: prepare_dirs
	gcc -c -g -ggdb -o build/debug_wrapper.o src/debug_wrapper.c
	nasm -f elf64 -g -F dwarf src/io_module.asm -o build/io_module.o
	nasm -f elf64 -g -F dwarf src/main.asm -o build/main.o
	gcc -g -o bin/main build/debug_wrapper.o build/main.o build/io_module.o -no-pie

Пример для проекта с математической библиотекой (FPU):

all: prepare_dirs
	gcc -c -g -ggdb -o build/debug_wrapper.o src/debug_wrapper.c
	nasm -f elf64 -g -F dwarf src/main.asm -o build/main.o
	gcc -g -o bin/main build/debug_wrapper.o build/main.o -no-pie -lm

📌 Для FPU программ: добавлен флаг -lm для линковки с математической библиотекой.

Проверка правильности изменений

После редактирования выполните:

make clean && make

Успешная сборка выглядит так:

gcc -c -g -ggdb -o build/debug_wrapper.o src/debug_wrapper.c
nasm -f elf64 -g -F dwarf src/main.asm -o build/main.o
gcc -g -o bin/main build/debug_wrapper.o build/main.o -no-pie

Проверьте, что программа работает:

./bin/main

Частые ошибки

❌ Ошибка 1: undefined reference to 'main'
/usr/bin/ld: warning: cannot find entry symbol _start

Причина: Объектные файлы указаны в неправильном порядке.

Решение: Убедитесь, что debug_wrapper.o стоит первым:

gcc -g -o bin/main build/debug_wrapper.o build/main.o -no-pie
                   ^^^^^^^^^^^^^^^^^^^^^ первым!
❌ Ошибка 2: Breakpoints не срабатывают или «прыгают»

Причина: Забыли добавить флаг -no-pie.

Решение: Добавьте -no-pie в конец команды линковки:

gcc -g -o bin/main build/debug_wrapper.o build/main.o -no-pie
                                                      ^^^^^^^
❌ Ошибка 3: error: debug_wrapper.c: No such file or directory

Причина: Файл debug_wrapper.c не создан или находится не в src/.

Решение: Убедитесь, что файл существует:

ls -la src/debug_wrapper.c

Если файла нет, вернитесь к Шагу 2 и создайте его.

Сводная таблица изменений

Аспект Финальная версия Версия для отладки
Компиляция C ❌ Нет gcc -c -g -ggdb src/debug_wrapper.c
Линковщик ld -m elf_x86_64 gcc -g ... -no-pie
Порядок .o Любой debug_wrapper.o первым
Доп. флаги Нет -no-pie (всегда), -lm (только для FPU)

🎯 6. Процесс отладки

Как запустить отладку

⚠️ Важное ограничение: Breakpoint’ы не работают напрямую в .asm файлах. Отладка выполняется через C-обёртку.

Шаги для запуска:

  1. Откройте src/debug_wrapper.c в VS Code
  2. Поставьте breakpoint (F9) на строке asm_main();
  3. Нажмите F5 или выберите конфигурацию 🔍 Debug
  4. Программа остановится на breakpoint’е в C-файле
  5. Нажмите F11 (Step Into) — вы попадёте в ассемблерную функцию
  6. Используйте F10 (Step Over) для пошагового выполнения
  7. Вводите данные в DEBUG CONSOLE когда программа их запросит

Горячие клавиши

Клавиша Действие
F5 Запустить отладку
F9 Поставить/убрать breakpoint
F10 Шаг с обходом (step over)
F11 Шаг с заходом (step into)
Shift+F11 Шаг с выходом (step out)

Что вы увидите

Отладка ассемблера в VS Code

Ключевые панели:

  • VARIABLES/WATCH (слева вверху) — значения переменных и регистров
  • CALL STACK (слева внизу) — цепочка вызовов: main()asm_main() → ваши функции
  • DEBUG CONSOLE (внизу) — ввод/вывод программы и команды GDB
  • Редактор (центр) — текущая строка выполнения (жёлтая стрелка)

🔍 7. Просмотр переменных в WATCH

Таблица типов данных

Тип в NASM Эквивалент C Выражение для WATCH
resb 1 char / int8_t *(char *)&var
resb 1 unsigned char / uint8_t *(unsigned char *)&var
resw 1 short / int16_t *(short *)&var
resw 1 unsigned short / uint16_t *(unsigned short *)&var
resd 1 int / int32_t *(int *)&var
resd 1 unsigned int / uint32_t *(unsigned int *)&var
resq 1 long long / int64_t *(long long *)&var
resq 1 unsigned long long / uint64_t *(unsigned long long *)&var
dd 0.0 float *(float *)&var
dq 0.0 double *(double *)&var

Примеры для разных типов программ

Целочисленные вычисления (int16 → int32)

Signed вариант:

*(short *)&a
*(short *)&b
*(int *)&output

Unsigned вариант:

*(unsigned short *)&a
*(unsigned short *)&b
*(unsigned int *)&output
Работа с массивами
// Первые 10 элементов массива
*(long long(*)[10])&array

// Конкретный элемент
*((long long *)&array + 5)

// Счётчик элементов
*(long long *)&count
FPU вычисления

В панели WATCH:

*(float *)&var_a
*(float *)&var_b
*(int *)&var_c

В DEBUG CONSOLE:

info float    # Все регистры FPU
print $st0    # Вершина стека FPU

💻 8. Полезные команды GDB

Вводите эти команды в DEBUG CONSOLE для расширенного контроля.

# Регистры
info registers              # Все регистры CPU
info float                  # Регистры FPU

# Память
x/10xb &array              # 10 байт в hex
x/10dw &array              # 10 слов в decimal
x/10xq &array              # 10 qwords в hex

# Работа с регистрами
print $rax                 # Значение
print/x $rax               # В hex
print/t $rax               # В binary
set $rax = 42              # Изменить

📖 Что значат эти регистры?

Если вы не помните, за что отвечает RFLAGS или RSP, загляните в раздел «🧠 2. Архитектура регистров x86-64» в «Шпаргалке NASM».

💡 Подробное руководство по GDB: «Настройка NASM x86-64 и VS Code: Гайд для Linux и Windows»

🛠️ 9. Troubleshooting

Многие проблемы, особенно Segmentation Fault при отладке, возникают из-за логических, а не синтаксических ошибок. Рекомендую ознакомиться со статьёй «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты», где разобраны типичные «тихие» баги.

❌ Ошибка: Cannot find program

Решение:

  1. Проверьте путь в launch.json: "${workspaceFolder}/bin/main"
  2. Запустите: make clean && make
  3. Убедитесь, что файл существует: ls -la bin/
❌ Breakpoint не срабатывает

Решение:

  1. Убедитесь в наличии флагов отладки: -g -F dwarf (NASM), -g -ggdb (GCC)
  2. Пересоберите: make clean && make
  3. Ставьте breakpoint на строке с инструкцией, не на комментарии
❌ WATCH показывает неверные значения

Решение: Проверьте соответствие типов:

  • resbchar (int8_t) / unsigned char (uint8_t)
  • reswshort (int16_t) / unsigned short (uint16_t)
  • resdint (int32_t) / unsigned int (uint32_t)
  • resqlong long (int64_t) / unsigned long long (uint64_t)
❌ Segmentation Fault при отладке

Возможные причины:

  1. Невыровненный стек (особенно для FPU программ)
  2. Неинициализированная память
  3. Выход за границы массива
  4. Неправильный доступ к argv (для программ с аргументами)
❌ Для программ с аргументами: программа не видит аргументы

Решение:

  1. Проверьте launch.json: массив args должен быть заполнен
  2. Убедитесь, что адаптировали код для получения argc/argv через регистры
  3. Проверьте, что используете правильные смещения: [rsi + 8], [rsi + 16], …

🔄 10. Подготовка к финальной версии

Когда отладка завершена, откатите все временные изменения:

Шаг 1: Удалите C-обёртку

rm src/debug_wrapper.c

Шаг 2: Верните _start в .asm файлах

Для всех программ:

Версия для отладки
section .text
    global asm_main

asm_main:
    ; Ваш код
    
    mov rax, 60
    xor rdi, rdi
    syscall
Финальная версия
section .text
    global _start

_start:
    ; Ваш код БЕЗ ИЗМЕНЕНИЙ
    ; syscall остаётся как был!
    
    mov rax, 60
    xor rdi, rdi
    syscall

Шаг 3: Для программ с аргументами — верните доступ через стек

Если вы адаптировали код для asm_main, верните обратно:

Версия для отладки
asm_main:
    ; При входе: rdi = argc, rsi = argv
    cmp     rdi, 2
    jl      .error_usage
    
    push    rsi
    mov     rdi, [rsi + 8]
    ; ...
Финальная версия
_start:
    pop     rax                       ; argc
    cmp     rax, 2
    jl      .error_usage
    
    pop     rbx                       ; argv[0]
    pop     rdi                       ; argv[1]
    ; ...

Шаг 4: Верните финальный Makefile

Для всех программ используйте ld для линковки:

all: prepare_dirs
	nasm -f elf64 -g -F dwarf src/main.asm -o build/main.o
	# другие .asm файлы...
	ld -m elf_x86_64 -o bin/main build/main.o # другие .o

prepare_dirs:
	@mkdir -p build bin

clean:
	@rm -f build/*.o bin/*

Шаг 5: Проверьте финальную сборку

make clean
make
./bin/main  # для программ без аргументов
./bin/main data/test.txt  # для программ с аргументами

🏁 11. Чеклист перед финализацией

Общий чеклист

  • Удалён src/debug_wrapper.c
  • Точка входа изменена с asm_main на _start
  • Makefile использует ld для линковки
  • syscall для выхода на месте (не заменён на ret)
  • Программа собирается и работает: make clean && make
  • Проверены все граничные случаи
  • Код прокомментирован

Дополнительно для программ с аргументами

  • Восстановлен доступ к argc/argv через стек (pop инструкции)
  • Удалены push rsi / pop rsi если они были добавлены
  • Проверена работа с разными вариантами аргументов
  • Программа корректно обрабатывает файлы

✅ Заключение

Этот подход позволяет:

Использовать современные инструменты — графический отладчик VS Code
Видеть состояние программы — переменные, регистры, память
Пошагово выполнять код — breakpoints, step into/over/out
Быстро находить ошибки — call stack, watch expressions
Соблюдать требования — финальная версия остаётся чистым ассемблером

Ключевые принципы

  1. C-обёртка не управляет выполнением — она только запускает asm_main
  2. Программа завершается через syscall — остаётся в ассемблерном коде
  3. Для простых программ процесс минимален — только замена _start на asm_main
  4. Для программ с аргументами нужна адаптация — argc/argv передаются через регистры rdi/rsi
  5. Временные изменения минимальны — легко откатить перед финализацией
💜

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

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

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