У меня есть следующий исходный файл 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, я получаю в конце несколько дополнительных байтов, и я не могу понять, почему. (Байты, которые находятся внутри заштрихованной области и прямоугольника ниже, являются ожидаемыми, все остальное неучтено.).
[]
Я подозреваю, что это какое-то статическое хранилище, используемое GCC. Я думал, что это может быть из-за строки в программе. Таким образом, я прокомментировал строку print("Hello world$");
, но лишние байты остались. Будет очень полезно, если кто-то знает, что происходит, и расскажет, как предотвратить вставку GCC этих байтов в вывод.
Исходный код доступен здесь: Гитхаб
PS: объектный файл также содержит эти дополнительные байты.
@PeterCordes Это верно. Спасибо. У вас есть идеи, почему лишние байты?
Понятия не имею, я не настолько знаком со скриптами компоновщика. Вы проверили .o
, чтобы увидеть, присутствуют ли эти байты в объектном файле? Я заметил, что вы не указали опцию --oformat
, поэтому вы не сказали LD создать плоский двоичный файл без метаданных.
@PeterCordes Да, они тоже есть в объектном файле.
Я бы не стал использовать gcc
для ссылки на исполняемый файл, а вместо этого просто вызывал бы ld
напрямую. Таким образом, вам не нужно беспокоиться о добавлении дополнительных объектных файлов или библиотек.
@RossRidge: -nostdlib
не включает все, включая libgcc
. Однако это может не означать -static
без -no-pie
. В любом случае, стоит попытаться удалить возможные, но маловероятные причины, но если это не окажется проблемой gcc -nostdlib
, в целом все должно быть хорошо, если вы не против передать все свои параметры компоновщика через -Wl
Поскольку вы используете собственный компилятор, а не кросс-компилятор i686 (или i386), вы можете получить достаточно дополнительной информации. Это скорее зависит от конфигурации компилятора. Я бы рекомендовал сделать следующее, чтобы удалить нежелательную генерацию кода и разделы:
-fno-asynchronous-unwind-tables
, чтобы удалить любые .eh_frame
разделы. В данном случае это причина нежелательных данных, добавленных в конце вашей DOS-программы COM.-static
для сборки без перемещений, чтобы избежать любой формы динамического связывания.--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, поэтому они не выводятся в исполняемом файле.
@RossRidge: Эта рекомендация на самом деле больше связана с тем, чтобы не загромождать соответствующий файл ELF (если они решили его сгенерировать) ненужными разделами, которые могут сбить их с толку или заставить их поверить, что они также стали частью двоичного выходного файла.
Дополнительные данные, вероятно, представляют собой информацию раскрутки DWARF. Вы можете запретить GCC генерировать его с помощью опции -fno-asynchronous-unwind-tables
.
Вы также можете сделать так, чтобы компоновщик GNU отбросил информацию о раскрутке, добавив следующее в директиву SECTIONS вашего скрипта компоновщика:
/DISCARD/ :
{
*(.eh_frame)
}
Также обратите внимание, что сгенерированный COM-файл будет на один байт больше, чем вы ожидаете, из-за нулевого байта в конце строки.
Примечание: ваша функция печати небезопасна. Разве
int $0x21
не возвращается в AL? Но вы не сказали об этом компилятору, только АХ. Лучше скажи ему, что весь EAX затерт.