Зачем нужна .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 шт):

Автор решения: PRODIGY

Вопрос: зачем нужна эта директива?

Директива ассемблера 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
→ Ссылка