Дополнительные байты в конце файла DOS .COM, скомпилированного с помощью GCC

У меня есть следующий исходный файл C с некоторыми блоками как м, которые реализуют процедуру печати и выхода, вызывая системные вызовы DOS.

__asm__(
    ".code16gcc;"
    "call dosmain;"
    "mov $0x4C, %AH;"
    "int $0x21;"
);

void print(char *str)
{
    __asm__(
        "mov $0x09, %%ah;"
        "int $0x21;"
        : // no output
        : "d"(str)
        : "ah"
    );
}

void dosmain()
{
    // DOS system call expects strings to be terminated by $.
    print("Hello world$");
}

Файл сценария компоновщика и файл сценария сборки, как таковые,

OUTPUT_FORMAT(binary)
SECTIONS
{
    . = 0x0100;
    .text :
    {
        *(.text);
    }
    .data :
    {
        *(.data);
        *(.bss);
        *(.rodata);
    }
    _heap = ALIGN(4);
}
gcc -fno-pie -Os -nostdlib -ffreestanding -m16 -march=i386 \
-Wl,--nmagic,--script=simple_dos.ld simple_dos.c -o simple_dos.com

Я привык создавать .COM-файлы на ассемблере и знаю структуру dos-файла. Однако в случае файла .COM, созданного с использованием GCC, я получаю в конце несколько дополнительных байтов, и я не могу понять, почему. (Байты, которые находятся внутри заштрихованной области и прямоугольника ниже, являются ожидаемыми, все остальное неучтено.).

Дополнительные байты в конце файла DOS .COM, скомпилированного с помощью GCC

[Дополнительные байты в конце файла DOS .COM, скомпилированного с помощью GCC]

Я подозреваю, что это какое-то статическое хранилище, используемое GCC. Я думал, что это может быть из-за строки в программе. Таким образом, я прокомментировал строку print("Hello world$");, но лишние байты остались. Будет очень полезно, если кто-то знает, что происходит, и расскажет, как предотвратить вставку GCC этих байтов в вывод.

Исходный код доступен здесь: Гитхаб

PS: объектный файл также содержит эти дополнительные байты.

Примечание: ваша функция печати небезопасна. Разве int $0x21 не возвращается в AL? Но вы не сказали об этом компилятору, только АХ. Лучше скажи ему, что весь EAX затерт.

Peter Cordes 29.05.2019 22:50

@PeterCordes Это верно. Спасибо. У вас есть идеи, почему лишние байты?

Arjob Mukherjee 29.05.2019 23:04

Понятия не имею, я не настолько знаком со скриптами компоновщика. Вы проверили .o, чтобы увидеть, присутствуют ли эти байты в объектном файле? Я заметил, что вы не указали опцию --oformat, поэтому вы не сказали LD создать плоский двоичный файл без метаданных.

Peter Cordes 29.05.2019 23:15

@PeterCordes Да, они тоже есть в объектном файле.

Arjob Mukherjee 29.05.2019 23:21

Я бы не стал использовать gcc для ссылки на исполняемый файл, а вместо этого просто вызывал бы ld напрямую. Таким образом, вам не нужно беспокоиться о добавлении дополнительных объектных файлов или библиотек.

Ross Ridge 30.05.2019 01:17

@RossRidge: -nostdlib не включает все, включая libgcc. Однако это может не означать -static без -no-pie. В любом случае, стоит попытаться удалить возможные, но маловероятные причины, но если это не окажется проблемой gcc -nostdlib, в целом все должно быть хорошо, если вы не против передать все свои параметры компоновщика через -Wl

Peter Cordes 30.05.2019 12:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
6
183
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Поскольку вы используете собственный компилятор, а не кросс-компилятор i686 (или i386), вы можете получить достаточно дополнительной информации. Это скорее зависит от конфигурации компилятора. Я бы рекомендовал сделать следующее, чтобы удалить нежелательную генерацию кода и разделы:

  • Используйте опцию GCC -fno-asynchronous-unwind-tables, чтобы удалить любые .eh_frame разделы. В данном случае это причина нежелательных данных, добавленных в конце вашей DOS-программы COM.
  • Используйте опцию GCC -static для сборки без перемещений, чтобы избежать любой формы динамического связывания.
  • Попросите GCC передать параметр --build-id=none компоновщику с помощью -Wl, чтобы избежать ненужного создания каких-либо .note.gnu.build-id разделов.
  • Измените скрипт компоновщика, чтобы ОТМЕНИТЬ любые .comment разделы.

Ваша команда сборки может выглядеть так:

gcc -fno-pie -static -Os -nostdlib -fno-asynchronous-unwind-tables -ffreestanding \
-m16 -march=i386 -Wl,--build-id=none,--nmagic,--script=simple_dos.ld simple_dos.c \
-o simple_dos.com

Я бы изменил ваш скрипт компоновщика, чтобы он выглядел так:

OUTPUT_FORMAT(binary)
SECTIONS
{
    . = 0x0100;
    .text :
    {
        *(.text*);
    }
    .data :
    {
        *(.data);
        *(.rodata*);
        *(.bss);
        *(COMMON)
    }
    _heap = ALIGN(4);

    /DISCARD/ : { *(.comment); }
}

Помимо добавления директивы /DISCARD/ для удаления любых .comment разделов, я также добавляю *(COMMON) рядом с .bss. Оба являются разделами BSS. Я также переместил их после разделов данных, так как они не будут занимать место в файле .COM, если появятся после других разделов. Я также изменил *(.rodata); на *(.rodata*); и *(.text); на *(.text*);, потому что GCC может генерировать имена разделов, которые начинаются с .rodata и .text, но имеют разные суффиксы.


Встроенная сборка

Не связано с проблемой, о которой вы спрашивали, но важно. В этой встроенной сборке:

__asm__(
    "mov $0x09, %%ah;"
    "int $0x21;"
    : // no output
    : "d"(str)
    : "ah"
);

Целое 21ч/AH=9ч также забивает АЛ. Вы должны использовать ax как клоббер.

Поскольку вы передаете адрес массива через регистр, вы также захотите добавить memory clobber, чтобы компилятор реализовал весь массив в памяти до того, как ваша встроенная сборка будет испущена. Ограничение "d"(str) только сообщает компилятору, что вы будете использовать указатель в качестве входных данных, а не то, на что указывает указатель.

Скорее всего, если вы скомпилировали с оптимизацией в -O3, вы, вероятно, обнаружите, что в следующей версии программы даже нет вашей строки "Hello world$" из-за этой ошибки:

__asm__(
        ".code16gcc;"
        "call dosmain;"
        "mov $0x4C, %AH;"
        "int $0x21;"
);

void print(char *str)
{
        __asm__(
                "mov $0x09, %%ah;"
                "int $0x21;"
                : // no output
                : "d"(str)
                : "ax");
}

void dosmain()
{
        char hello[] = "Hello world$";
        print(hello);
}

Сгенерированный код для dosmain выделил место в стеке для строки, но никогда не помещал строку в стек перед печатью строки:

00000100 <print-0xc>:
 100:   66 e8 12 00 00 00       calll  118 <dosmain>
 106:   b4 4c                   mov    $0x4c,%ah
 108:   cd 21                   int    $0x21
 10a:   66 90                   xchg   %eax,%eax

0000010c <print>:
 10c:   67 66 8b 54 24 04       mov    0x4(%esp),%edx
 112:   b4 09                   mov    $0x9,%ah
 114:   cd 21                   int    $0x21
 116:   66 c3                   retl

00000118 <dosmain>:
 118:   66 83 ec 10             sub    $0x10,%esp
 11c:   67 66 8d 54 24 03       lea    0x3(%esp),%edx
 122:   b4 09                   mov    $0x9,%ah
 124:   cd 21                   int    $0x21
 126:   66 83 c4 10             add    $0x10,%esp
 12a:   66 c3                   retl

Если вы измените встроенную сборку, включив в нее "memory" клобер, например:

void print(char *str)
{
        __asm__(
                "mov $0x09, %%ah;"
                "int $0x21;"
                : // no output
                : "d"(str)
                : "ax", "memory");
}

Сгенерированный код может выглядеть как аналогичный:

00000100 <print-0xc>:
 100:   66 e8 12 00 00 00       calll  118 <dosmain>
 106:   b4 4c                   mov    $0x4c,%ah
 108:   cd 21                   int    $0x21
 10a:   66 90                   xchg   %eax,%eax

0000010c <print>:
 10c:   67 66 8b 54 24 04       mov    0x4(%esp),%edx
 112:   b4 09                   mov    $0x9,%ah
 114:   cd 21                   int    $0x21
 116:   66 c3                   retl

00000118 <dosmain>:
 118:   66 57                   push   %edi
 11a:   66 56                   push   %esi
 11c:   66 83 ec 10             sub    $0x10,%esp
 120:   67 66 8d 7c 24 03       lea    0x3(%esp),%edi
 126:   66 be 48 01 00 00       mov    $0x148,%esi
 12c:   66 b9 0d 00 00 00       mov    $0xd,%ecx
 132:   f3 a4                   rep movsb %ds:(%si),%es:(%di)
 134:   67 66 8d 54 24 03       lea    0x3(%esp),%edx
 13a:   b4 09                   mov    $0x9,%ah
 13c:   cd 21                   int    $0x21
 13e:   66 83 c4 10             add    $0x10,%esp
 142:   66 5e                   pop    %esi
 144:   66 5f                   pop    %edi
 146:   66 c3                   retl

Disassembly of section .rodata.str1.1:

00000148 <_heap-0x10>:
 148:   48                      dec    %ax
 149:   65 6c                   gs insb (%dx),%es:(%di)
 14b:   6c                      insb   (%dx),%es:(%di)
 14c:   6f                      outsw  %ds:(%si),(%dx)
 14d:   20 77 6f                and    %dh,0x6f(%bx)
 150:   72 6c                   jb     1be <_heap+0x66>
 152:   64 24 00                fs and $0x0,%al

Альтернативная версия встроенной сборки, которая передает подфункцию 9 через ограничение a с использованием переменной и помечает ее как ввод/вывод с помощью + (поскольку возвращаемое значение AX затирается), может быть выполнена следующим образом:

void print(char *str)
{
    unsigned short int write_fun = (0x09<<8) | 0x00;
    __asm__ __volatile__ (
        "int $0x21;"
        : "+a"(write_fun)
        : "d"(str)
        : "memory"
    );
}

Рекомендация: не используйте GCC для генерации 16-битного кода. Встроенная сборка — трудно понять правильно, и вы, вероятно, будете использовать ее изрядное количество для подпрограмм низкого уровня. В качестве альтернативы вы можете посмотреть на Меньший C, Компилятор C Брюса или Опенватком С. Все они могут генерировать DOS COM-программы.

В секциях .comment не установлен флаг ALLOC, поэтому они не выводятся в исполняемом файле.

Ross Ridge 30.05.2019 06:56

@RossRidge: Эта рекомендация на самом деле больше связана с тем, чтобы не загромождать соответствующий файл ELF (если они решили его сгенерировать) ненужными разделами, которые могут сбить их с толку или заставить их поверить, что они также стали частью двоичного выходного файла.

Michael Petch 30.05.2019 06:59

Дополнительные данные, вероятно, представляют собой информацию раскрутки DWARF. Вы можете запретить GCC генерировать его с помощью опции -fno-asynchronous-unwind-tables.

Вы также можете сделать так, чтобы компоновщик GNU отбросил информацию о раскрутке, добавив следующее в директиву SECTIONS вашего скрипта компоновщика:

/DISCARD/ : 
{
     *(.eh_frame)
}

Также обратите внимание, что сгенерированный COM-файл будет на один байт больше, чем вы ожидаете, из-за нулевого байта в конце строки.

Другие вопросы по теме