Зачем нужна .org 0x7C00 директива для запуска кода со старта? Запуск кода с первого сектора диска
Во многих истчниках пишут, что в код ассемблера необходимо вставить директиву с указанием адреса .org 0x7C00
, якобы код начинает исполняться с этого адреса (точнее bios туда его заносит с первого сектора диска). Эта директива присутствует и в исходниках ядра линукса, и во многих других, например, https://github.com/fffaraz/bootloader/blob/master/bootloader.asm (тут, кстати, nasm сразу выдает файл размером 512 байт, тоже непонятно почему, догадка одна: автоматически "трунканул". Это "кстати" относится к тексту после линковки).
Теория гласит: нужно установить 16ти битный режим, BIOS читает самый первый сектор, проверяет 511 и 512 байты на наличие 0x55 и 0xAA соответственно, если проверка успешна, то код размещается в адрес 0x0000:0x7C00 и начинает исполняться.
Я банально пытаюсь вывести 'X' на экран при старте, вот код и последующие шаги:
.org 0x7C00
.code16
start:
mov $'X', %al
mov $0xE, %ah
int $0x10
cli
hlt
. = start + 510
.byte = 0x55
.byte = 0xAA
Компилирую:
as -o boot.o boot.S
Получаю objdump -D
boot.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_start-0x7c00>:
...
0000000000007c00 <_start>:
7c00: b0 58 mov $0x58,%al
7c02: b4 0e mov $0xe,%ah
7c04: cd 10 int $0x10
7c06: fa cli
7c07: f4 hlt
...
7dfc: 00 00 add %al,(%rax)
7dfe: 55 push %rbp
7dff: aa stos %al,%es:(%rdi)
Disassembly of section .note.gnu.property:
0000000000000000 <.note.gnu.property>:
0: 04 00 add $0x0,%al
2: 00 00 add %al,(%rax)
4: 20 00 and %al,(%rax)
6: 00 00 add %al,(%rax)
8: 05 00 00 00 47 add $0x47000000,%eax
d: 4e 55 rex.WRX push %rbp
f: 00 02 add %al,(%rdx)
11: 00 01 add %al,(%rcx)
13: c0 04 00 00 rolb $0x0,(%rax,%rax,1)
...
1f: 00 01 add %al,(%rcx)
21: 00 01 add %al,(%rcx)
23: c0 04 00 00 rolb $0x0,(%rax,%rax,1)
27: 00 01 add %al,(%rcx)
29: 00 00 add %al,(%rax)
2b: 00 00 add %al,(%rax)
2d: 00 00 add %al,(%rax)
Удаляю .note.gnu.property
strip --remove-section=.note.gnu.property boot.o
Линкую:
ld --oformat binary -o boot boot.o
На выходе получаю файл размером 32256 байта. Хе, еще бы, код ведь с 0x7C00 начинается! Глядя hexdump этого файла, видно, что до этого "адреса" нули. И тут же встает вопрос, как же люди записывают этот файл потом в первый сектор 512 байт? Конечно же никак, кто-то что-то недоговаривает, даже qemu-system-i386 -drive format=raw,file=boot
не запустит этот файл, кода в первых 512 байтах не будет
Если же убрать .org 0x7C00
, то итоговый файл будет размера ровно 512 байт. Этот файл уже может запустить qemu.
Как передать управление коду с usb на старте компьютера. Я делаю так:
Для этого я использую утилиту dd. Команда:
dd if=boot of=/dev/sdb bs=512 count=1 conv=notrunc
Всё записывается, hexdump так же срабатывает для /dev/sdb, содержимое первых 512 идентичны. qemu может запустить /dev/sdb
В bios отключаю безопасный режим, включаю поддержку CSM (там ведь uefi уже везде), готово, перехожу на вкладку boot, bios видит эту флешку (KingstonDatatraveler 3.0, кстати), выбираю её, запускаю, напечатан заветный символ 'X'. Код сработал без .org 0x7C00.
Вопрос: зачем нужна эта директива?
UPD:
Сейчас изменил 0xAA на 0xAF и код все равно прочитался и запустился. Я понимаю это так: bios (без uefi) копирует в RAM только первые 440 байт первого сектора (https://wiki.archlinux.org/title/Arch_boot_process), значит последующие он не читает и 0x55 и 0xAA не имеют значения, они нужны для uefi.
Вопрос: правильно ли я понял, или в чем дело?
Ответы (1 шт):
Вопрос: зачем нужна эта директива?
Директива ассемблера ORG
устанавливает базовое смещение в коде, для инструкций переходов по "абсолютным адресам" типа call
и jmp
. Если в коде таковых нет, для бинарных файлов можно не указывать её. Чтобы понять детали нужно вспомнить, как собственно компиляторы кодируют переходы?
Процессоры в режиме х16/32 использовали 2 формы прыжков - по абсолютным, и по относительным адресам. Что касается х64, то здесь уже нет абсолютных, и все переходы RIP-относительные, с 32-битным операндом назначения. Инструкцию jmp
Intel описывает так:
- 1. Near (ближний) — переход к инструкции внутри текущего сегмента CS (внутрисегментный).
- 2. Short (короткий) — тот же Near, но диапазон прыжка ограничен +/– 127 байт от текущего значения (E)IP.
- 3. Far (дальний) — переход к инструкции, расположенной в отличном от текущего CS сегменте, иногда называемый межсегментым.
- 4. Task swich (переключение задачи) — переход к инструкции, расположенной в другой задаче.
Здесь Near (опкод Е9h) и Short (опкод ЕBh) являются "относительными переходами". Отличие их в том, что у Near операнд 2-байтный, а у Short он в виде 1-байтного значения со-знаком. Если этот байт имеет отрицательное значение, то переход осуществляется назад от текущего (E)IP
, если положительное, то соответственно это прыжок вперёд. Что касается FAR (опкод ЕАh) - это всегда переход по "абсолютному адресу". Операнд имеет 2-байтное значение в режиме х16, и 4-байтное в режиме х32.
Рассмотрим такой код загрузчика, и сразу вскормим его дизассемблеру (здесь главное безусловный переход jmp
, и условный loop
):
org 0x7c00
jmp start
var1 db 0
var2 db 0
;//---------- Точка входа
start: xor ax,ax
mov ds,ax
mov ss,ax
mov sp,0x7c00
;//---------- Создадим цикл
mov cx,10
@@: mov ax,word[var1]
loop @b
;//---------- Переход типа Short
jmp @next
@next: nop
nop
;//---------- Переход типа Far
db 0xEA
dw @absolute
@absolute:
xor ax,ax
ret
А вот его дизассм вид:
* Entry Point:
00007C00: EB02 jmp 00007C04 ;// Short, операнд(02) положительное значение - вперёд!
00007C02: 0000 add [bx+si], al
00007C04: 31C0 xor ax, ax
00007C06: 8ED8 mov ds, ax
00007C08: 8ED0 mov ss, ax
00007C0A: BC007C mov sp, 7C00
00007C0D: B90A00 mov cx, 000A
00007C10: A1027C mov ax, [7C02]
00007C13: E2FB loop 00007C10 ;// Операнд(FB) отрицательное значение - назад!
00007C15: EB00 jmp 00007C17 ;// Short, операнд положительное значение - вперёд!
00007C17: 90 nop
00007C18: 90 nop
00007C19: EA1C7C jmp 00007C1C ;// FAR, операнд абсолютный адрес!
00007C1С: 31C0 xor ax, ax
00007C1E: C3 ret
При относительной адресации, значение операнда хранит смещение в диапазоне +/- 127 от текущего EIP
, который всегда указывает на сл.инструкцию в коде, что доказывает опкод Short в дизассме выше. Но что мы видим в случае дальнего перехода FAR? Компилятор волшебным образом вычислил правильное значение перехода 0x7C1C
. От куда он взял его?
В этом заслуга как-раз директивы ORG - благодаря именно ей компиль получает валидные "абсолютные адреса" физ.памяти. Если убрать ORG из исходника, то получим бинарник ниже, с указывающим в космос переходом CS:001C
, в то время как база загрузчика в ОЗУ = CS:7С00
.
00000010: A10200 mov ax, [0002]
00000013: E2FB loop 00000010
00000015: EB00 jmp 00000017
00000017: 90 nop
00000018: 90 nop
00000019: EA1C00 jmp 0000001C
0000001С: 31C0 xor ax,ax
0000001E: C3 ret