[Перевод] Linux и TinyСC в браузере
Несмотря на то, что Clang in Browser довольно впечатляет, большинство существующих браузерных компиляторов все же находятся в экспериментальной стадии. Я же в этом проекте вместо портирования компилятора в WASM собираюсь пойти своим излюбленным путем — задействовать виртуальные машины. Этот способ не такой уж быстрый, особенно ввиду использования мной JS-эмулятора ЦПУ, но при этом все же позволяет добиться отличной производительности с помощью шустрого компилятора вроде Tiny С Compiler и кастомизированного Linux.
Демо
Живая версия демо доступна в оригинале статьи
Мотивация проекта
Было время, когда я мог часами сидеть и настраивать Linux на своем Pentium, стремясь ускорить загрузку системы. В большинстве случаев я просто все ломал и был вынужден перекомпилировать Gentoo. Однако потребность компилировать Linux возникает редко. Если вам нужно что-то в чистом виде, то вы наверняка используете Docker с Alpine Linux. Тем не менее компиляция Linux сохраняет свою актуальность во вложенных пространствах, а при участии в этом процессе компилятора Си можно дополнительно разобраться и в основах работы программ.
Между тем в качестве альтернативы Docker появились такие unikernel-продукты, как MirageOS и Unikraft. Одно из их отличий в том, что ваш код компилируется в операционную систему, а не выполняется поверх Linux. Представьте себе возможность компилировать Linux в свой код, исключая при этом его неиспользуемые фрагменты, относящиеся к неактуальному для вас функционалу. Это дает преимущества в виде уменьшения поверхности атаки, ускорения загрузки и повышения быстродействия.
В таком случае сборка кастомного Linux становится еще более увлекательной, поскольку unikernel заимствуют у Linux множество концепций. Например, Unikraft конфигурируется в том же TUI, что и ядро Linux (и Buildroot), в нем активно используются Make и GCC, плюс представлена возможность выбора между множеством реализаций libc, но что же конкретно подразумевает подобная сборка?
О чем руководство
В этом руководстве вы узнаете, как скомпилировать небольшой образ Linux для выполнения в браузере через v86, 32-битный эмулятор ЦПУ x86, написанный на JS. В процессе вы познакомитесь с кросс-компиляцией при помощи современной реализации стандартной библиотеки Си, а также с некоторыми внутренними особенностями этого языка. В конце описывается удаленная отладка через GDB с помощью gdbserver, виртуальных последовательных портов и QEMU.
Что потребуется
Linux (не wsl), хотя бы час на компиляцию и следующие пакеты для Buildroot:
sudo apt install make gcc g++ libncurses-dev libssl-dev
Я выполнял сборку в Ubuntu 20.04 и 22.04 с помощью bash, но для этого вполне подойдет большинство современных дистрибутивов.
Перед началом создайте каталог для проекта, например, ~/my-v86-linux. Затем перейдите в него через cd
и выполняйте все команды оттуда. Назвать этот каталог можете на свое усмотрение, и он не обязательно должен находиться в ~/.
Эмулятор v86
v86 выполняется в браузере, эмулируя x86-совместимые ЦПУ и оборудование, на котором машинный код переводится в модули WebAssembly в среде выполнения. Список эмулируемого аппаратного обеспечения довольно внушителен:
- набор инструкций x86, аналогичный Pentium III;
- поддержка клавиатуры и мыши;
- VGA-карта;
- IDE-контроллер;
- сетевая карта;
- файловая система virtio;
- звуковая карта.
Полный список можно посмотреть в v86 readme.
Использование этого эмулятора не предполагает исключительно Linux. Он вполне работает с Windows (1.01, 3.1, 95, 98, 2000), ReactOS, FreeBSD, OpenBSD и различными любительскими ОС.
v86 — это хобби-проект, написанный анонимным разработчиком под псевдонимом «copy». Судя по его странице, помимо эмулятора copy также реализовал какой-то особо сложный платформер, игру «Жизнь» и написал на JS интерпретатор Brainfuck.
Buildroot
Buildroot — это инструмент для генерации встроенных систем Linux с помощью кросс-компиляции. Он представляет собой масштабную работу компилирующих скриптов, объединенную с файлами конфигурации в приятном интерфейсе терминала, откуда вы можете настраивать практически все.
Помимо этого, Buildroot выступает как настраиваемый набор утилит, предоставляя все необходимые инструменты для кросс-компиляции приложений, отсутствующие в штатных пакетах Buildroot.
Подробнее на сайте https://buildroot.org.
Что ж, приступим.
Перейдите в каталог проекта, затем скачайте и извлеките Buildroot:
Подсказка: вместо использования мыши, просто перебирайте команды с помощью Tab и копируйте их.
mkdir buildroot
wget https://github.com/buildroot/buildroot/archive/refs/tags/2022.02.1.tar.gz \
--output-document - \
| tar -xz --strip-components 1 --directory buildroot
Вместо сборки Linux на основе предустановленной конфигурации Buildroot, мы используем шаблон, устанавливающий нужные ЦПУ, архитектуру и прочие компоненты:
wget https://github.com/humphd/browser-vm/archive/refs/tags/v1.0.2.tar.gz \
--output-document - \
| tar -xz --strip-components 1 browser-vm-1.0.2/buildroot-v86
Удалите команды для сжатия информации лицензий, мы вернемся к этому позднее.
echo "" > buildroot-v86/board/v86/post-image.sh
Попросите Buildroot создать новый файл .config с предварительно загруженными настройками из шаблона:
make --directory buildroot BR2_EXTERNAL=../buildroot-v86 v86_defconfig
Теперь все почти готово для сборки начального образа. Выполните:
make --directory buildroot menuconfig
Перейдите в Toolchain → C library и выберите musl, exit и save. Можете запускать сборку.
make --directory buildroot
Это займет некоторое время, но зато благодаря включенному кэшированию в следующий раз процесс пойдет быстрее.
Немного скажу о musl… Это реализация стандартной библиотеки Си, наподобие uClibc и glibc. В вашем дистрибутиве, вероятно, используется glibc, GNU С Library, которая довольно громоздкая и не особо подходит для встроенного Linux, где размер играет важную роль. uClibc здесь более уместна также, как musl, которая в этом (предвзятом) сравнении выглядит откровенным победителем.
Я предпочитаю musl под лицензией MIT, а не (L)GPL, что делает ее интересной для проприетарных приложений, выполняющихся в unikernel. Разработал ее и поддерживает Рич Фелкер при содействии многих участников, а в этом подкасте говорится (на 01:01:17), что в случае системного программирования исходный код musl можно использовать в качестве эталона.
Подготовка сайта
Пока Buildroot занят компиляцией, мы создадим сайт, где будет располагаться эмулятор, и выполняться Buildroot Linux:
mkdir web
wget https://github.com/copy/v86/releases/download/latest/libv86.js \
--directory-prefix web
wget https://github.com/copy/v86/releases/download/latest/v86.wasm \
--directory-prefix web
wget https://github.com/copy/v86/releases/download/latest/v86-fallback.wasm \
--directory-prefix web
wget https://github.com/copy/v86/archive/refs/tags/latest.tar.gz \
--output-document - \
| tar -xz --strip-components 2 --directory web \
v86-latest/bios/seabios.bin \
v86-latest/bios/vgabios.bin
cat >web/index.html <
Emulator
EOF
После завершения компиляции выполните:
cp buildroot/output/images/rootfs.iso9660 web/linux.iso
Далее откройте терминал и запустите простой сервер, указывающий на веб-каталог, например:
python3 -m http.server 8000 --directory web
Откройте http://localhost:8000, чтобы увидеть v86 в действии. Авторизуйтесь как рут-пользователь, пароль не потребуется.
Настройка образа
Вся суть Buildroot в кастомизации. Попробуйте следующие команды:
make --directory buildroot menuconfig
make --directory buildroot busybox-menuconfig
make --directory buildroot linux-menuconfig
Здесь есть, что поизучать.
menuconfig
Этот инструмент используется для настройки таких элементов, как версия ядра Linux, используемый загрузчик (grub2, syslinux и т.д.), стандартная библиотека и целевая архитектура для компиляции. Выбрать здесь можно из нескольких пакетов, которые простираются от небольших библиотек и утилит до X11 и Qt.
busybox-menuconfig
Busybox совмещает в одном исполняемом файле сотни утилит Linux, а также предлагает богатые возможности конфигурирования с помощью busybox-menuconfig
. Она предоставляет ls, grep, diff и многие другие привычные утилиты из Linux, и я рекомендую удалить все ненужные инструменты, чтобы минимизировать размер итогового образа. В идеале Busybox должен изначально содержать голый минимум, не требуя ручного удаления всего лишнего. Именно здесь и проявляется удобство unikernel, поскольку в этих системах используется противоположный подход, когда вы начинаете практически без ничего и сами добавляете все необходимое.
linux-menuconfig
В этой утилите производится настройка ядра Linux. Здесь присутствует огромное множество компонентов, и внесение корректировок без должного понимания может повлечь сбой работы системы. В одном из последующих руководств этой серии я покажу, как настраивать ядро на примере проб и ошибок, поскольку делаю так сам: удаляю одну функцию, тестирую систему, сплевываю через плечо и повторяю.
Пока советую воздержаться от соблазна внести изменения.
rootfs_overlay
Этот каталог располагается в buildroot-v86/board/v86/rootfs_overlay, и именно в него мы помещаем файлы, которые хотим добавить в образ. Наш текущий шаблон содержит два таких файла: etc/fstab и etc/inittab.
Отключение сообщений ядра после авторизации
Некоторые вещи на загрузку никак не влияют, но все равно являются частью этого процесса. Они могут медленно запускаться и загромождать терминал после авторизации, вставляя сообщения журнала прямо в середину вводимой команды.
Чтобы отключить такие сообщения ядра после авторизации, создайте следующий файл:
mkdir buildroot-v86/board/v86/rootfs_overlay/etc/profile.d
echo "echo 0 >/proc/sys/kernel/printk" \
>buildroot-v86/board/v86/rootfs_overlay/etc/profile.d/noprintk.sh
Все файлы .sh из etc/profile.d выполняются при авторизации.
Автоматическая авторизация
Команда etc/inittab подготавливает файловую систему и монтирует etc/fstab
, запускает скрипты инициализации и «порождает» приложения после загрузки. Одна из команд для порождения заканчивается комментарием # GENERIC_SERIAL
— эту строчку нужно изменить, чтобы не запрашивать авторизацию, а просто запускать /bin/sh
.
(F=buildroot-v86/board/v86/rootfs_overlay/etc/inittab && cp $F /tmp/oldf \
&& sed --in-place "28d" $F \
&& sed --in-place "s/.*# GENERIC_SERIAL/console::respawn:-\/bin\/sh/" $F \
&& diff /tmp/oldf $F)
Заметьте, что эта команда начинается с console::respawn
. Возрождение (respawn) означает, что если sh
даст сбой, Busybox продолжит ее перезапускать, пока она не завершится с успехом.
Замена getty
здесь происходит, потому что авторизацию запрашивает само приложение. Это также избавляет нас от отправки сообщений между tty, что имеет смысл только в многопользовательской системе: если пользователь А авторизован в tty1, а пользователь B в tty2, тогда А не должен иметь возможности потревожить B, выполнив echo "Hi B!" >/dev/tty2
. Вместо этого мы порождаем -/bin/sh
, где дефис требует от Busybox рассматривать оболочку как оболочку с авторизацией. В противном случае /etc/profile
и скрипты в /etc/profile.d
будут проигнорированы.
Для добавления файлов в образ нужно просто повторить компиляцию.
Добавление Tiny C Compiler
Tiny C Compiler, или tcc, можно описать как:
Я использовал tcc для компиляции приложений win32 с opengl и gdi+, а также pdf-библиотеки, которую мы позднее задействуем для оценки производительности. При этом у компиляции есть свои ограничения, к примеру я не смог скомпилировать libpng, но вы можете использовать GCC для предоставления общей библиотеки, с которой tcc сможет связаться.
Этот компилятор написал Фабрис Беллар, автор QEMU, ffmpeg, quickjsm, kslinux и прочих программ. Так или иначе, вы наверняка пользовались его ПО. Я же здесь возьму последнюю версию tcc, которую он выпустил до прекращения поддержки проекта. Тем не менее она вполне жива и доступна в этом форке.
Чтобы заставить tcc работать, его придется скомпилировать дважды: первый раз для компиляции libtcc1.a. Согласно Makefile, сначала здесь для компиляции tcc используется GCC, после чего tcc собирает и выводит libtcc1.a. Если начать с компиляции musl, то он не будет выполняться на хосте, а значит libtcc1.a собрать не получится.
Поэтому первым шагом будет настройка сборки с помощью опции --enable-cross
, которая приведет к созданию кросс-компилятора, компилирующего правильный libtcc1.a.
После этого можно будет выполнить компиляцию для одной архитектуры и libc: x86 musl.
mkdir tcc
wget http://download.savannah.gnu.org/releases/tinycc/tcc-0.9.27.tar.bz2 \
--output-document - \
| tar -xj --strip-components 1 --directory tcc \
--exclude tests --exclude examples
mkdir libtcc
cp --recursive tcc/* libtcc
Настраиваем кросс-компиляцию tcc для текущей архитектуры ЦПУ, чтобы получить i386-версию libtcc1.a.
(cd libtcc && ./configure --prefix=./output --enable-cross)
В glibc 2.34 хуки malloc были исключены, и Ubuntu 22.04 поставляется с glibc 2.35. Следующие две команды не являются необходимыми для Ubuntu 20.04, но при этом вполне безвредны.
(F=libtcc/lib/bcheck.c && cp $F /tmp/oldf \
&& sed --in-place "s/#define CONFIG_TCC_MALLOC_HOOKS//" $F \
&& sed --in-place "s/#define HAVE_MEMALIGN//" $F \
&& diff /tmp/oldf $F)
Далее собираем libtcc на хосте и копируем в каталог оверлея.
make --directory libtcc
make --directory libtcc install
mkdir -p buildroot-v86/board/v86/rootfs_overlay/lib/tcc
cp libtcc/output/lib/tcc/i386-libtcc1.a \
buildroot-v86/board/v86/rootfs_overlay/lib/tcc/libtcc1.a
Следующим шагом идет настройка и сборка компилятора для x86 musl.
(cd tcc && ./configure \
--cpu=x86 \
--config-musl \
--cross-prefix=${PWD}/../buildroot/output/host/bin/i686-buildroot-linux-musl- \
--elfinterp=/lib/ld-musl-i386.so.1 \
--crtprefix=/lib \
--libdir=/lib \
--tccdir=/lib/tcc \
--bindir=/bin \
--includedir=/include \
--sysincludepaths=/lib/tcc/include:/include \
--sharedir=-unused \
`# Нам нужно выполнить отладку символов для дальнейшей работы, но в продакшен-сборке это поле необходимо раскомментрировать.` \
`# В итоге размер файла уменьшится на ~70%.` \
`# --strip-binaries`)
make --directory tcc \
--assume-old libtcc1.a \
--assume-old tcc-doc.html \
--assume-old tcc-doc.info
DESTDIR=$PWD/tcc/output make --directory tcc install
cp --recursive tcc/output/* buildroot-v86/board/v86/rootfs_overlay
Здесь:
--assume-old
заставляетmake
пропустить libtcc1.a. При этом также пропускаются требующие makeinfo шаги, поскольку документация в итоге окажется в каталогеoutput-unused
, что несколько хитро определяется с помощью--sharedir=-unused
.DESTDIR
устанавливается, потому что настройка с опцией--prefix=./output
приводит к компиляции tcc с путями поиска, начинающимися с этого префикса.--elfinterp
указывает на динамический линковщик в образе, отвечающий за обнаружение необходимых приложению общих библиотек, подготавливает его к запуску и затем выполняет. Так как мы используем musl, этот файл называется ld-musl-i386.so.1, но в вашем дистрибутиве на базе glibc это наверняка будет ld-linux-x86–64.so.2. Без него система не будет знать, как запускать приложения, и вы получите/bin/sh: {your command}: not found
.
Для создания исполняемых файлов tcc необходимы подпрограммы запуска, связанные с этими файлами. Названия таких подпрограмм начинаются с crt, сокращение от «с runtime», и мы настроили tcc на поиск их в /lib. Поскольку tcc поддерживает выполнение Си без создания исполняемого файла через есс -run file.c
, эти файлы потребуются вам, только если вы хотите создавать исполняемые файлы (и если планируете следовать продолжению этого руководства). Вот краткие сведения о crt-файлах из https://dev.gentoo.org/~vapier/crt.txt:
crt1.o
Содержит символ _start
, который конфигурирует env
с argc/argv/libc _init/libc _fini
до перехода к libc main.
crti.o
Определяет пролог функции; _init
в разделе .init
и _fini
в разделе .fini
.
crtn.o
Определяет эпилог функции.
cp buildroot/output/host/i686-buildroot-linux-musl/sysroot/lib/crt*.o \
buildroot-v86/board/v86/rootfs_overlay/lib
Вот все, что нужно для запуска tcc в v86, но без предоставляемых musl стандартных заголовков Си он способен не на многое. Мы берем только голый минимум, потому что все заголовки без сжатия весят ~5МБ.
printf "buildroot/output/host/i686-buildroot-linux-musl/sysroot/usr/include/%s\0" \
bits alloca.h assert.h complex.h ctype.h errno.h fenv.h float.h features.h \
inttypes.h iso646.h limits.h locale.h math.h memory.h malloc.h setjmp.h \
signal.h stdalign.h stdarg.h stdbool.h stddef.h stdint.h stdio.h stdlib.h \
stdnoreturn.h string.h strings.h tgmath.h threads.h time.h uchar.h \
wchar.h wctype.h \
| xargs -0 cp --recursive --target buildroot-v86/board/v86/rootfs_overlay/include
Hello world
После компиляции и установки tcc в наш образ пора заняться подготовкой кода для его тестирования.
mkdir buildroot-v86/board/v86/rootfs_overlay/opt
cat >buildroot-v86/board/v86/rootfs_overlay/opt/test.c <
#include
int main(int argc, char **argv)
{
char *name = "stranger";
if (argc > 1 && strlen(argv[1]) > 0)
name = argv[1];
printf("Hello, %s\n", name);
return 0;
}
EOF
Пересобираем образ с новыми файлами:
make --directory buildroot
cp buildroot/output/images/rootfs.iso9660 web/linux.iso
Если вы закрыли сервер, откройте новый терминал и выполните:
python3 -m http.server 8000 --directory web
Перейдите в http://localhost:8000 и выполните в эмуляторе:
# Компиляция и выполнение без создания бинарника
tcc -run /opt/test.c
# Создание бинарника
tcc /opt/test.c -o hello
./hello world
Бенчмарк
Пора провести небольшой тест, чтобы понять, какую производительность можно ожидать. Для этого мы используем прекрасную pdf-библиотеку libhau.
mkdir libharu
wget https://github.com/libharu/libharu/archive/refs/tags/RELEASE_2_3_0.tar.gz \
--output-document - \
| tar -xz --strip-components 1 --wildcards --directory libharu \
"libharu-RELEASE_2_3_0/include/*.h" \
"libharu-RELEASE_2_3_0/src/*.c" \
libharu-RELEASE_2_3_0/src/t4.h \
libharu-RELEASE_2_3_0/demo/line_demo.c
cat >libharu/include/hpdf_config.h <
Выполняем sudo apt install sloccount
, а затем sloccount libharu
и узнаем, что эта библиотека состоит из 128394 физических строк кода. Все дело в на удивление больших файлах с массивами, содержащими данные кодирования. Но давайте посмотрим, сколько займет компиляция всего этого, создав быстрый и грубый бенчмарк, работающий и для GCC, и для tcc.
cat >libharu/benchmark <libharu/benchmark-link <
Прогон бенчмарков
Выполните локально:
libharu/benchmark gcc
Выполните в эмуляторе:
libharu/benchmark tcc # Наберитесь терпения
libharu/benchmark-link
Как вполне ожидаемо показывает тест, линковка с предварительно скомпилированной общей библиотекой оказывается быстрее, чем компилирование с нуля. На моей машине тестовая линковка в v86 заняла 60 мс. Неплохо! Взгляните на libharu/demo/line_demo.c — это не далеко не самый маленький файл Си.
Я намеренно не стал показывать, как компилировать совместно используемую библиотеку с помощью tcc. Здесь есть кое-какой баг, и мы его в следующем разделе разберем.
Отладка
Если вы следовали всем этапам до этого момента, то можете открыть эмулятор и выполнить:
tcc -shared -fPIC -Ilibharu/include libharu/src/*.c
Эта команда просит tcc скомпилировать вместо исполняемого файла общую библиотеку и длится около 30 секунд, после чего завершается с ошибкой сегментации.
Я не скажу вам, как исправить эту проблему, потому что у меня нет потребности в компиляции с помощью tсс общих библиотек на кастомной системе x86, да и понимания для ее исправления мне не достает. Правда, осознал я это не сразу и поначалу попытался выяснить причину сбоя, для чего потребовалась…
Удаленная отладка
Отладчик GNU — GDB — поддерживает удаленную отладку через gdbserver, представляющий собой небольшое приложение, которое вы запускаете на целевой машине для последующего подключения к нему из GDB. Было бы круто запустить gdbserver в v86 внутри браузера и подключиться к нему из GDB, но поскольку GDB в v86 не работает (чуть позже вы узнаете, почему), gdbserver не заработает тоже.
Поэтому, чтобы заняться отладкой, нам потребуется воспроизвести возникший баг в QEMU и создать для связи GDB/gdbserver виртуальный последовательный порт с помощью socat. А для компиляции GDB нам потребуется использовать musl-cross-make через Git.
sudo apt install qemu-system-i386 socat git
После установки QEMU можно без проблем запустить образ:
qemu-system-i386 -serial stdio -cdrom web/linux.iso -cpu Westmere
И здесь мы даже получаем прикольную консоль для копипаста. Но на этом хорошие новости заканчиваются, и пора переходить к плохим…
Buildroot, GDB и musl не особо друг с другом ладят, что в случае выбора пакета GDB приводит к ошибкам конфигурации. В связи с этим нам нужно скомпилировать GDB самостоятельно, используя другой набор инструментов. Этого можно было избежать, задействовав вместо musl пакет uClibc, но во имя лицензии MIT мы получили то, что получили. Надеюсь, что вы не будете сильно возражать против еще одного серьезного этапа компиляции.
Код ниже клонирует musl-cross-make, настроит и скомпилирует ее.
git clone https://github.com/richfelker/musl-cross-make.git
cat >musl-cross-make/config.mak <
Можете отвлечься на кофе.
…
Теперь мы готовы к сборке GDB/gdbserver с помощью инструментов, установленных в musl-cross-make/output/bin
. В данном случае оптимальной версией для компиляции будет GDB 10.2, так как он не требует GMP (GNU Multiple Precision Arithmetic Library), необходимую для последующих версий.
mkdir gdb
wget https://ftp.gnu.org/gnu/gdb/gdb-10.2.tar.gz --output-document - \
| tar -xz --strip-components 1 --directory gdb
(cd gdb && \
PATH=$PATH:$PWD/../musl-cross-make/output/bin \
./configure \
--prefix=$PWD/output \
--host=i686-linux-musl \
--disable-nls \
--with-curses)
PATH=$PATH:$PWD/musl-cross-make/output/bin make --directory gdb -j$(nproc)
PATH=$PATH:$PWD/musl-cross-make/output/bin make --directory gdb install
Новый набор инструментов в musl-cross-make/output/bin
соблюдает соглашение об именовании для кросс-компиляторов, поэтому каждая программа начинается с i686-linux-musl
, что устанавливается в конфигурации musl-cross-make/config.mak
с помощью TARGET
. GDB следует тому же соглашению и благодаря установке i686-linux-musl
в –host
, а также добавлению этого набора инструментов в PATH
, GDB сможет обнаружить правильные утилиты без необходимости установки их в вашу систему. Помимо этого, мы --disable-nls
(отключаем локализацию) и компилируем --with-curses
, вместо использования древней предустановленной альтернативы, подразумевающей раздельную компиляцию.
С помощью strip
очищаем gdbserver от отладочных символов и несущественных данных, после чего копируем на целевую машину. Это уменьшает размер файла gdbserver с 8МБ до 500КБ. Для запуска gdbserver также потребуется стандартная библиотека C++.
Все вместе эти файлы весят ~2500КБ, так что после отладки их снова нужно будет удалить.
Далее необходимо скомпилировать GDB для хоста, что несложно сделать в Buildroot:
make --directory buildroot menuconfig
Затем выберите Toolchain → Build cross gdb for the host и cкомпилируйте:
make --directory buildroot
cp buildroot/output/images/rootfs.iso9660 web/linux.iso
Qemu и виртуальные последовательные порты
Пока происходит компиляция, мы создаем псевдо-терминал (pty), выступающий в качестве виртуального последовательного порта. Поскольку socat использует для терминалов случайные id вроде /dev/pty/2
и /dev/pty/18
, мы просим его создать для этих случайных id символические ссылки с заранее известными нам id.
Откройте новый терминал и выполните:
socat pty,rawer,link=/tmp/vserial-host pty,rawer,link=/tmp/vserial-target
Когда компиляция завершится, запустите QEMU в новом терминале и подключитесь к виртуальному последовательному порту хоста:
qemu-system-i386 -serial stdio -cdrom web/linux.iso -cpu Westmere \
-chardev serial,id=gdbserial,path=/tmp/vserial-host \
-device isa-serial,chardev=gdbserial
Если вы введете в последовательной консоли dmesg | grep tty
, то увидите два порта: ttyS0
, подключенный к вашему терминалу через -serial stdio
, и ttyS1
, подключенный к виртуальному порту socat.
Запустите gdbserver в последовательной консоли QEMU для отладки tcc:
gdbserver /dev/ttyS1 tcc -shared -fPIC -Ilibharu/include libharu/src/*.c
Затем запустите GDB на хосте, указав на кросс-компилированную версию tcc:
buildroot/output/host/bin/i686-buildroot-linux-musl-gdb \
-ix buildroot/output/staging/usr/share/buildroot/gdbinit \
tcc/output/bin/tcc
-ix
означает: до inferior
— если коротко, то так в GDB зовется процесс — выполнить файл buildroot/.../gdbinit
. При этом gdbinit
предоставляется Buildroot и содержит следующее:
add-auto-load-safe-path {...}/buildroot/output/host/i686-buildroot-linux-musl/sysroot
set sysroot {...}/buildroot/output/host/i686-buildroot-linux-musl/sysroot
Здесь указывается каталог на целевой машине, содержащий копии библиотек в соответствующих подкаталогах.
Далее подключаемся к QEMU и запускаем tcc:
(gdb) target remote /tmp/vserial-target
(gdb) continue
Вы получите несколько предупреждений, которые, как я понял, вызваны выполняемой Buildroot очисткой общих библиотек от символов отладки. Далее появится следующая ошибка:
0x004f9c1f in fill_local_got_entries (s1=0xb7e99020) at tccelf.c:1362
1362 for_each_elem(s1->got->reloc, 0, rel, ElfW_Rel) {
Если заглянуть в исходный код tcc, мы увидим, что он выполняется только при компиляции общих библиотек. Возможно, здесь влияет повторная компиляция для uClibc или апгрейд до форка tcc (для чего требуется дополнительная работа, связанная с компиляцией). Дайте знать, если вам удастся исправить проблему, чтобы я мог внести эту информацию в статью.
Можно было вместо этого добавить GDB в rootfs_overlay
и запустить его в QEMU, но тогда мы теряем фрагменты кода ошибки ввиду отсутствия исходных файлов. Если же вас устраивает присутствие только номеров строк, то без проблем можете использовать GDB на целевой машине.
Отладка в v86
Мне не удалось заставить GDB работать в v86. При каждой попытке отладки все компоненты выдают segfault
. Смена набора инструментов на uClibc позволяет Buildroot скомпилировать GDB, но проблему это не решает, а понижение версии с 11.2 на 10.2 или 8 вообще ничего не меняет. В QEMU отладчик работает, так что дело должно быть в v86. Было бы здорово, если бы GDB сообщал, что именно дало сбой при выполнении, но пока придется обойтись компилятором Си.
Лицензии
Для получения всех лицензий от Buildroot, выполняем:
make --directory buildroot legal-info
В итоге они оказываются в buildroot/output/legal-info. Получение полного списка лицензий для всего, используемого здесь, оставлю вам в качестве упражнения.