
Комментарии 13
Вроде как -device loader в qemu должен честно грузить и запускать ELF согласно заголовкам.
Не вроде, а так и есть.
Loading Files
^^^^^^^^^^^^^
The loader device also allows files to be loaded into memory. It can load ELF,
U-Boot, and Intel HEX executable formats as well as raw images.
Более того в данном случае можно запускать с ключём -bios, тоже будет работать.
Если запускать бинарник c произвольным entry point, тогда:
-device loader,addr=<address>,file=<file>,cpu-num=0 -device loader,addr=<entry_point>,cpu-num=0
Что имел ввиду автор я после первого прочтения не понял.
https://github.com/riscv-software-src/riscv-tests без проблем запускаются с ключом -bios=файл или через loader.
Проверил -bios, работает, спасибо.
Статья вот про что: машина стартует туда, куда зашито, и файл на это не влияет, ровно как на кристалле (по крайней мере я на это надеюсь). Ради этого весь стенд и стоит на -bios none. А -bios и loader с cpu-num=0 это возможности эмулятора, которых у железа нет: они дают файлу распорядиться тем, откуда машина стартует. В статье это надо было сказать прямо, а одна фраза там и вовсе говорит «QEMU не читает» вместо «-kernel не читает». Допишу дополнение в текст.
Проверил, вы правы: с cpu-num=0 точка входа читается. Одну фразу в статье надо было сказать точнее, там «QEMU не читает», а верно «-kernel не читает».
А пока проверял, нашлось кое-что поинтереснее. Вот слово по 0x1018, которое переходник машины читает перед прыжком:
-bios none -kernel hello.elf 0x80000000
-bios none -kernel high.elf 0x80000000
-bios hello.elf 0x80000000
-bios high.elf 0x80100000hello.elf собран по 0x80000000, high.elf по 0x80100000. Под -kernel переходник машины не меняется вообще: какой файл ни дай, управление уйдёт туда, куда зашито. А -bios этот переходник переписал адресом из заголовка.
Похоже, в этом и вся разница. -bios и loader с cpu-num=0 дают файлу распорядиться тем, откуда машина стартует, а на кристалле адрес сброса зашит в железо. Стенд у меня стоит на -bios none как раз ради этого, и читать заголовок там просто некому.
Досюда всё про QEMU, а на плате читать заголовок обычно уже некому: objcopy -O binary срезает их физически, у меня из 5824 байт ELF остаётся 472 байта прошивки, и сигнатуры \x7fELF в ней нет. Правда, -O ihex и -O srec точку входа всё-таки несут, отдельной записью (:040000058000001C5B и S7058000001C5E), так что зависит от формата. А дальше от того, кто грузит: U-Boot в bootelf прыгает именно на e_entry (lib/elf.c, return ehdr->e_entry), а вот load_image в OpenOCD только кладёт байты, адрес там задаётся отдельной resume. Похоже, поле читает не машина, а загрузчик, и вопрос всегда в том, есть ли он. Своей платы у меня нет, на железе не проверял.
Как дотянусь до железок перепроверю(но это не точно)
а верно «-kernel не читает»
А -kernel и не должен. Это опция для загрузки U-Boot Proper или Linux kernel после отработки встроенного OpenSBI. А они вообще в S-MODE уже стартуют. Это первое.
Второе -M virt (впрочем как и другие RISC-V машины в QEMU) по умочанию стартуют с mrom (небольшой код в ~8 инструкций который содержит адрес fw_info -> https://elixir.bootlin.com/qemu/v11.1.0-rc3/source/hw/riscv/boot.c#L483.
Третье - тогда как loader в принципе обходит эту цепочку и его команды означают:
-device loader,addr=<address>,file=<file>,cpu-num=0
Загрузи мне блоб по определнному адресу, и используя первый cpu (у cpu вообще говоря могут быть разные адресные пространства, например imx7d).
-device loader,addr=<address>,cpu-num=0
Принудительно задавай reset-vector указанное значение.
Четвертое, по умолчанию reset_vector для -M virt 0x1000 mrom который потом сам дойдет до 0x80000000 и всё собственно.
А вот если сделать:
$ build-qemu/qemu-system-riscv32 -M virt -bios none -device loader,file=riscv-tests/isa/rv32ui-p-add,addr=0x80001000 -device loader,addr=0x80001000,cpu-num=0 -nographic -serial mon:stdio -s -S
QEMU 11.0.0 monitor - type 'help' for more information
(qemu) info registers
CPU#0
V = 0
pc 80001000
Пожалуйста - принудильно установили PC в 0x80001000 для cpu0.
Про физику писать ничего не буду - там и так всё понятно - вот reset_vector, будь добр чтобы твоя программа начиналась и работала именно с этого адреса. Хотя там можно загрузить через gdb который сам позабодиться о pc === entry_point.
Про gdb вы правы. Я это пробовал выяснить и бросил: в руководстве про счётчик команд ни слова или я не нашел, а до исходника не добрался, sourceware отдавал мне заглушку. Сейчас достал с зеркала на gitlab, gdb/symfile.c, generic_load:
CORE_ADDR entry = bfd_get_start_address (loadfile_bfd.get ());
...
regcache_write_pc (get_thread_regcache (inferior_thread ()), entry);Ставит. Между этими строками там ещё шесть, и в них gdb печатает «Start address …», то есть он про точку входа даже вслух говорит.
Вторую форму loader я вообще не пробовал. Проверил на своей ловушке, она печатает X по 0x80000000, а _start у неё на 0x8000001c:
-device loader,file=decoy.elf X
+ -device loader,addr=0x8000001c,cpu-num=0 fib(1..10): 1 1 2 3 5 8 13 ...
+ -device loader,addr=0x80000000,cpu-num=0 XРаботает ровно как вы написали.
Про «-kernel и не должен» спорить не буду, тем более с вашим опытом и знаниями. Я описал следствие, вы назначение.
Раз уж полез в boot.c, мелочь: инструкций там 6, reset_vec[0…5], дальше данные, start_addr в [6] и dtb в [8]. Я это место когда-то дампил через монитор, сошлось.
За строчку про физику спасибо отдельно, платы у меня нет и вопрос так и висел в заметках.
Обычно так не бывает, чтобы точку входа игнорируют. При том, что я плохо понял статью (сейчас не могу её внятно прочитать), то относился бы к этой информации с кепсисом или, если это действтительно так, то изучал досконально момент где она игнорируется в ядре линукса.
Посмотрел чуть-чуть репозиторий, вы каждый файл ассемблера отдельно компилируете? Если так, то везде будет 0x8000000, надо либо объектные файлы, а потом собирать их вместе, либо один большой файл с ассемблером.
Забудьте, что я написал вторым абзацем, там глупость. Попробовал повторить опыт с изменением стартовой точки и у меня получилось следующее:
Скрытый текст
Правильный экзешник
m039@DESKTOP-FC78JS1:~/Code/test$ readelf -h ./a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 13968 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 14
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30Измененный виртуальный адрес, изменил с помощью хекс редактора.
m039@DESKTOP-FC78JS1:~/Code/test$ readelf -h ./a2.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x101060
Start of program headers: 64 (bytes into file)
Start of section headers: 13968 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 14
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30Там теперь `Entry point address: 0x101060`.
Пытаюсь его запустить:
m039@DESKTOP-FC78JS1:~/Code/test$ ./a2.out
-bash: ./a2.out: Permission denied
m039@DESKTOP-FC78JS1:~/Code/test$ qemu-x86_64 ./a2.out
Error while loading /home/m039/Code/test/a2.out: Exec format errorКажется, я как-то не так все понял и не могу повторить эксперимент. У меня с измененной стартовой точкой не хочет запускаться экзешник, ни в линуксе, ни в qemu.
Можно еще поизучать исходники qemu и поискать там обращения к e_entry.
Спасибо, что полезли проверять руками.
У вас всё получилось. Просто результат читается наоборот: вы показали, что в Linux точку входа как раз читают. Повторил ваш опыт на Ubuntu, тот же PIE, тот же сдвиг восьми байт по смещению 24:
оригинал, +x ok
точка входа сдвинута, +x Segmentation fault
точка входа сдвинута, без +x Permission deniedPermission denied тут не про заголовок. Файл с нетронутым заголовком и снятым правом на исполнение даёт ту же самую ошибку: хекс-редактор сохранил новый файл без +x. Сделайте chmod +x, и получите Segmentation fault, а это и есть доказательство, что заголовок прочитали и ушли по нему в пустоту.
Про Exec format error под qemu-x86_64 уверенности нет, поэтому предположением: у PIE сегменты LOAD кончаются в районе 0x4000, а 0x101060 лежит за пределами всего образа, и загрузчик qemu-user, видимо, бракует файл ещё на разборе. Сам проверить не смог, qemu-user под рукой не встал.
Где заголовок читают, там поле работает. У меня стенд стоит на -bios none, то есть без всякой прошивки и загрузчика, и читать его там просто некому. Как на плате, куда залили сырой образ. Но мне это еще стоит проверить на живой плате, но как писал ранее это не точно. Всё зависит от того что смогу ли я получить плату.
Да, не заметил как chmod +x упустил, так то я его делал, но на другом экзешнике, но все равно ошибку выдает, что говорит что линукс и qemu читают стартовую точку.
m039:~/Code/test$ chmod +x a2.out
m039:~/Code/test$ ./a2.out
Segmentation fault ./a2.out
m039:~/Code/test$ qemu-x86_64 ./a2.out
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
Segmentation fault qemu-x86_64 ./a2.outДа, я это место не проверил. Сейчас выбрал время и разобрался.
Поставил qemu-user и прогнал все четыре сочетания:
точка входа цела, +x ok
точка входа сдвинута, +x сигнал 11
точка входа цела, 644 отказ, код 1
точка входа сдвинута, 644 отказ, код 1Без права на исполнение отказ одинаковый и от точки входа не зависит вообще. С правом всё решает точка входа. Так что вы правы, а моё предположение про то, что qemu-user бракует файл на разборе, было не верно, он его грузит и уходит по подменённому адресу.
А заодно нашлось, почему сообщение такое обманчивое. linux-user/linuxload.c: prepare_binprm за отсутствующий +x возвращает честный -EACCES, но вызывающий loader_exec делает так:
retval = prepare_binprm(bprm);
if (retval < 4) {
return -ENOEXEC;
}prepare_binprm при успехе возвращает число прочитанных байт, поэтому любой отрицательный код меньше четырёх и превращается в -ENOEXEC. Отсюда и Exec format error вместо Permission denied. У меня на 8.2.2 сообщения нет, просто код 1, так что тут ещё и от версии зависит.
Итого ваш опыт дал готовый список тех, кто точку входа читает: ядро Linux и qemu-user. С моей стороны к ним U-Boot и сам QEMU через -bios или loader с cpu-num=0. На стенде нет ни одного из них, поэтому там её и не читает никто.
Занятно, что расхождение вышло не в результатах, а в том, кто какой способ запуска держал в голове. Прогоны-то у всех сошлись, спор был про -kernel против -bios и loader, то есть про разные машины. Ну и тексты ошибок, как видно, ещё и от версии зависят.
На всякий случай - qemu-user и qemu-system разные вещи, общее у них только TCG:
- qemu-user эмуляция отдельного Linux-приложения. Системные вызовы транслируются в вызовы ядра хоста. Быстро, но работает только под Linux/BSD;
- qemu-sytem (так же называется softmmu) эмуляция целой системы: процессора, памяти, периферии (UART, GPIO, АЦП и т.д.). Именно этот режим позволяет "крутить" операционную систему для другой архитектуры;
Более того следует различать режимы baremetal без MMU где используются физчиеские адреса и с MMU используются виртуальные адреса и памятью управляет ОС.
Да, свалил в кучу. У меня они в списке стояли рядом, а это разные этажи: ядро и qemu-user читают e_entry запускаемой программы, а -bios и loader,cpu-num=0 задают счётчик команд машины при сбросе. Из одной строчки этого не видно.
Полез посмотреть, насколько разные, и залип. У PIE e_entry вообще не адрес. readelf показывает 0x1060 и не меняется никогда, а куда ядро реально уходит:
$ LD_SHOW_AUXV=1 ./a.out | grep AT_ENTRY
AT_ENTRY: 0x55580d478060
AT_ENTRY: 0x564f7a121060
AT_ENTRY: 0x556d3ed37060
Три запуска, три адреса. Все на 060, потому что база выровнена по странице, а 0x1060 к ней прибавляется. Смещение, а не адрес.
Под qemu-user база не прыгает, гость всегда 0x555555557060. Там, кстати, строк AT_ENTRY выходит две, и я сначала решил наугад, какая чья. Потом сообразил прогнать qemu-x86_64 --version: он печатает одну, и она каждый раз новая. Значит первая это сам qemu, вторая гость.
А у меня на стенде базы нет вообще. Без MMU что в файле, то и в памяти. Получается, случая три, а не два. Или я сам себя запутал, поправьте, если так.
В заголовке моего ELF есть точка входа. Оказалось, её никто не читает