Приветствую. Сегодня, немного оправившись от новогодних хлопот, предлагаю заняться самой что ни на есть компьютерной археологией и, возможно, открыть для себя ранее неизвестные удобные, полезные или просто интересные инструменты.
Всё ниже перечисленное также без особых сложностей можно проделать на Linux или FreeBSD
Зачем? "Just for fun" :)

Кросс-компилируем C код под DOS, OS/2 и старые Windows

А почему бы и нет. В этом нам поможет проект Open Watcom v2.

1. Окружение и сборка

Установим Wine, DOSBox-X и OrbStack для запуска созданных исполняемых ELF, DOS и PE файлов :

brew install dosbox-x wine-stable orbstack

Скачаем и установим Open Watcom V2:

git clone https://github.com/open-watcom/open-watcom-v2
cd open-watcom-v2

Перейдём на любую стабильную версию кода, так как в главной ветке иногда возможны проявляющиеся как внезапные Segmentation Fault в процессе сборки регрессии. В моём случае, например, так:

git checkout 4ac34aec0a48b5f9df3c703e0977227835d17a68

С помощью любого удобного текстового редактора создадим скрипт vars.sh со следующим содержимым:

vars.sh
#!/bin/bash

export OWROOT=$(realpath "`pwd`")
export OWTOOLS=CLANG
export OWDOCBUILD=0
export OWDISTRBUILD=0
export OWDOSBOX=dosbox-x
. "$OWROOT/cmnvars.sh"

С другими доступными переменными, влияющими на процесс сборки, можно ознакомиться в файле setvars.sh в корне репозитория.

Применим указанные настройки и запустим скрипт, который автоматически соберёт компиляторы и все вспомогательные инструменты и библиотеки:

source vars.sh
./build.sh

Сборка достаточно долгая, так как производится исключительно в однопоточном режиме и, например, на Apple M4 занимает около получаса.

2. Установка и настройка

После завершения сборки сгенерируем папку с исполняемыми файлами, библиотеками и заголовками:

./build.sh rel

И установим в систему, например вот так:

sudo mkdir -p /opt/watcom
sudo cp -vr rel/* /opt/watcom

Open Watcom поддерживает сборку для множества целей, так DOS 16/32bit, OS/2, windows 16/32bit, linux и некоторые другие, и для каждой системы в переменных окружения INCLUDE и LIB необходимо указывать пути к заголовкам и библиотекам для требуемой системы. Предполагая, что Open Watcom помещён в /opt/watcom, в файл конфигурации оболочки (~/.profile, ~/.bash_profile, ~/.zshrc или другой) предлагаю поместить вот такую функцию.

Функция настройки окружения Open Watcom
watcom() {
    export WATCOM=/opt/watcom
    export EDDAT=$WATCOM/eddat
    if [[ ":$PATH:" != *":$WATCOM/armo64:"* ]]; then
        export PATH="$WATCOM/armo64:$PATH"
    fi

    case "$1" in
        dos)
            export INCLUDE="$WATCOM/h"
            export LIB="$WATCOM/lib286/dos:$WATCOM/lib286"
            echo "Target: DOS 16-bit (Real Mode)"
            ;;
        dos32)
            export INCLUDE="$WATCOM/h"
            export LIB="$WATCOM/lib386/dos:$WATCOM/lib386"
            echo "Target: DOS 32-bit (Protected Mode / DOS/4GW)"
            ;;
        windows)
            export INCLUDE="$WATCOM/h:$WATCOM/h/win"
            export LIB="$WATCOM/lib286/win:$WATCOM/lib286"
            echo "Target: Windows 16-bit"
            ;;
        win32)
            export INCLUDE="$WATCOM/h:$WATCOM/h/nt"
            export LIB="$WATCOM/lib386/nt:$WATCOM/lib386"
            echo "Target: Windows 32-bit"
            ;;
        os2)
            export INCLUDE="$WATCOM/h:$WATCOM/h/os2"
            export LIB="$WATCOM/lib286/os2:$WATCOM/lib286"
            echo "Target: OS/2 16-bit"
            ;;
        os2v2)
            export INCLUDE="$WATCOM/h:$WATCOM/h/os2"
            export LIB="$WATCOM/lib386/os2:$WATCOM/lib386"
            echo "Target: OS/2 32-bit"
            ;;
        linux)
            export INCLUDE="$WATCOM/lh"
            export LIB="$WATCOM/lib386/linux:$WATCOM/lib386"
            echo "Target: Linux x86"
            ;;
        *)
            echo "Error: Unknown or missing target."
            echo "Usage: watcom [target]"
            echo "Available targets:"
            echo "  dos      - DOS 16-bit"
            echo "  dos32    - DOS 32-bit (DOS/4GW)"
            echo "  windows    - Windows 3.x"
            echo "  win32    - Windows 9x/NT/XP/10"
            echo "  os2      - OS/2 16-bit"
            echo "  os2v2      - OS/2 32-bit"
            echo "  linux    - Linux x86"
            ;;
    esac
}

Эта функция при необходимости будет добавлять пути к исполняемым файлам Open Watcom в PATH и устанавливать переменные среды с путями для выбранной системы. Не забываем применить обновлённый файл конфигурации оболочки перед выполнением следующих шагов.

3. Кросс-компиляция

В любом удобном месте создадим файл hello.c со следующим содержимым:

#include <stdio.h>
int main(void) {
  puts("Hello, world!\n");
  return 0;
}

Соберём .com файл для 16-битной DOS:

watcom dos
owcc -b com hello.c -o hello.com

Тип файла можно проверить так:

file hello.com
# выведет hello.com: DOS executable (COM)

А запустить так:

dosbox-x -hostrun hello.com

Или .exe для неё же:

owcc -b dos hello.c -o hello.exe
# MS-DOS executable, MZ for MS-DOS

Или даже для 32-битного защищённого режима:

watcom dos32
owcc -b dos4g hello.c -o hello.exe
# MS-DOS executable, LE executable

Для Windows, Linux и OS/2 аналогично:

watcom windows
owcc -b windows hello.c -o hello.exe
# MS-DOS executable, NE for MS Windows 3.x (EXE)

watcom win32
owcc -b win32 hello.c -o hello.exe
# PE32 executable (GUI) Intel 80386, for MS Windows
# можно запустить и проверить
wine hello.exe

watcom os2
owcc -b os2 hello.c -o hello.exe
# MS-DOS executable, NE for OS/2 1.x (EXE)

watcom os2v2
owcc -b os2v2 hello.c -o hello.exe
# MS-DOS executable, LX for OS/2 (console) i80386

watcom linux
owcc -b linux hello.c -o hello
# ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, with debug_info, not stripped

# Создадим виртуальную машину linux для проверки
orb start
orb create -a amd64 alpine linux-vm1
orb -m linux-vm1 ./hello

Список поддерживаемых целей можно увидеть в файле /opt/watcom/armo64/specs.owc. К слову, Open Watcom в определенной степени поддерживает стандарт c99, поэтому следующий код:

#include <stdio.h>
int main() {
puts("Hello, World!\n");
}

Собрать без ошибок и предупреждений можно так:

owcc -b dos -std=c99 hello.c -o hello.exe

Также Open Watcom имеет ограниченную поддержку C++, но почти любой современный проект потребует большого количества исправлений для подобной сборки, и мне не удалось найти способов интересно применить эту возможность.

Кросс-компилируем ANSI C для PDP-11, не привлекая внимания санитаров

Чтобы не ограничивать себя Windows, перейдём к следующему любопытному инструменту - ACK (Amsterdam compiler kit), который может компилировать код на ANSI C, Pascal, Modula 2, Basic и создавать программы для CP/M, EM22, Linux (i386, 68k, mips и ppc), minix, MSDOS (i86 и i386), osx (i386 и ppc), Raspberry Pi и, наш следующий герой, PDP-11.

Окружение и сборка

brew install python3 lua
git clone https://github.com/davidgiven/ack
cd ack

Откроем Makefile в любом текстовом редакторе, найдём строку PREFIX ?= /opt/pkg/ack и изменим путь установки по вкусу, например так:

PREFIX ?= /opt/ack

Введём make для начала сборки, которая на M4 с 10 ядрами занимает около трёх с половиной минут. После её завершения установим инструментарий ack в систему:

sudo make install

Добавим /opt/ack/bin в PATH и для проверки также соберём наш hello world:

ack -mlinux386 hello.c -o hello
# ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, not stripped

Крестики-нолики, или сказ о том, как выиграть битву, но проиграть войну

"Привет, мир" конечно хорошо, но почему бы не попробовать что-нибудь поинтереснее? Например, крестики-нолики :)
Для успешной сборки код должен быть написан на C89, и мне удалось найти этот проект.

git clone https://github.com/marcelog/ttt
cd ttt
ack -mlinux386 ttt.c libttt.c -o ttt
# ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, not stripped
orb -m linux-vm1 ./ttt

Надо же - работает!
"Но Linux - это пошло, скучно и примитивно" - скажете вы, и, конечно же, будете правы:

ack -mpdpv7 ttt.c libttt.c -o ttt
# PDP-11 executable not stripped

Запускаем? Конечно!

Установим эмулятор старых ЭВМ, в том числе и pdp11, и удобный загрузчик aria2c:

brew install open-simh aria2c

Далее нам нужно установить Unix V7 в симуляторе. Делать это будем согласно руководству.

1. Подготовка образа установочной ленты с Unix

Скачаем дистрибутив Unix V7:

# создадим рабочие директории
mkdir -p ~/pdp11-workspaces/{v7-work,v7-distribution-files}
cd ~/pdp11-workspaces/v7-distribution-files

# скачаем необходимые файлы
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f0.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f1.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f2.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f3.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f4.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f5.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/f6.gz
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/filelist
aria2c https://www.tuhs.org/Archive/Distributions/Research/Keith_Bostic_v7/mktape.pl

# создадим образ ленты
chmod u+x mktape.pl
cd ../v7-work
cp ../v7-distribution-files/* .
gunzip f?.gz
./mktape.pl
# При успехе в терминале отобразятся размеры блоков и записей для каждого файла f0-f6
# очистим ненужные файлы
rm f* mktape.pl

В результате в рабочей директории останется единственный файл v7.tap.

2. Запуск и первоначальная настройка симулятора PDP-11

Создадим файл конфигурации simh tape.ini для эмуляции PDP-11/45 с RP06-дисками и TU10-лентой:

Создание tape.ini
cat > tape.ini <<"EOF"
set cpu 11/45
set cpu idle
set rp0 rp06
att rp0 rp06-0.disk
set rp1 rp06
att rp1 rp06-1.disk
att tm0 v7.tap
boot tm0
EOF
  • Запустим симулятор командой pdp11 tape.ini

  • Согласимся на перезаписи, вводя y и нажимая enter.

  • Создадим файловую систему на первом созданном диске (корневая директория). Введём **tm(0,3). Для files sys size укажем 5000, а для file system - hp(0,0).

  • Восстановим root с ленты. Введём tm(0,4),. Для Tape укажем tm(0,5), а для Disk - hp(0,0). Подтвердим запись на диск нажатием enter.

  • Загрузимся с root.

    • Введём hp(0,0)hptmunix.

    • Исправим консоль, для чего введём STTY -LCASE NL0 CR0.

Далее необходимо создать файлы устройств для RP дисков, на которых и будет храниться система. Делается это так:

cp hptmunix unix
rm hphtunix rphtunix rptmunix

# создадим RP06-устройство
cd /dev
/etc/mknod rp0 b 6 0
/etc/mknod swap b 6 1
/etc/mknod rp3 b 6 15
/etc/mknod rrp0 c 14 0
/etc/mknod rrp3 c 14 15
chmod go-w rp0 swap rp3 rrp0 rrp3

# создадим TU10-устройство
make tm

Создадим файловую систему на втором диске (/usr):

cd /
/etc/mkfs /dev/rp3 322278
# и проверим её
icheck /dev/rp3

Восстановим /usr с ленты:

dd if=/dev/nrmt0 of=/dev/null bs=20b files=6
restor rf /dev/rmt0 /dev/rp3
# подтвердим нажатием enter

Смонтируем /usr и скопируем boot-блок:

/etc/mount /dev/rp3 /usr
dd if=/usr/mdec/hpuboot of=/dev/rp0 count=1
sync
# sync нужно ввести несколько раз

Остановим симулятор сочетанием клавиш ctrl+e и выйдем, введя q.

3. Передача данных в симулятор

Воспользуемся этим проектом, который значительно облегчит для нас задачу создания образа магнитной ленты и записи файлов с хоста.
Убедимся, что на машине установлены rust и cargo, вслед за этим в любой удобной папке выполним:

git clone https://github.com/nigeleke/mktape
cd mktape
cargo build -r --bin mktape
sudo cp -vr target/release/mktape /usr/local/bin

Вернёмся в рабочую папку и запишем полученный ранее исполняемый файл крестиков-ноликов на ленту. Но поскольку запись производится с выравниванием по 1 Кб, мы упакуем его в tar архив (при распаковке нули в конце файла архива проигнорируются), чтобы не нарушить структуру исполняемого файла,.

cd ~/pdp11-workspaces/v7-work
# скопируем исполняемый файл для создания архива, путь может отличаться
cp ~/ttt/ttt .
tar -cf ex.tar ttt
mktape transfer.tap create ex.tar

При успехе в терминале отобразится что-то подобное:
ex.tar: 23552 bytes = 23 records (blocksize 1024 bytes)
Число записей, в нашем случае 23, необходимо запомнить, пригодится позже.

4. Момент истины

Создадим файл конфигурации simh nboot.ini для нормальной загрузки с RP06 диска:

Создание nboot.ini
cat > nboot.ini <<"EOF"
set cpu 11/70
set cpu 2M
set cpu idle
set rp0 rp06
att rp0 rp06-0.disk
set rp1 rp06
att rp1 rp06-1.disk
att tm0 transfer.tap
boot rp0
EOF

Запустим эмулятор:

pdp11 nboot.ini

Введём boot, и далее hp(0,0)unix.
Для перехода в многопользовательский режим воспользуемся сочетанием клавиш ctrl+d, при запросе логина и пароля введём root.

Извлечём наш исполняемый файл игры:

# Читаем данные с ленты
# если число записей на прошлом шаге отличается, для count надо указать именно его
dd if=/dev/rmt0 of=ex.tar bs=1024 count=23
# распаковываем
tar xf ex.tar

# проверим, неужто?
file ttt
# ttt:    executable not stripped

./ttt

Когда надоест, завершить программу можно сочетанием клавиш ctrl+delete.


К сожалению, насладиться игрой не получилось из-за ошибок во время выполнения, которые, возможно, связаны с несовместимостью типов в коде, написанном явно не для 16-битной архитектуры процессора. Пропатчить его предлагаю читателю :)

5. Бонус: извлечение данных с RP диска на хост-машине

Скачаем в любую удобную папку набор файлов для Unix V7, включающий в себя в том числе утилиту для работы с дисками, используемыми в PDP-11, затем извлекём нужную утилиту, соберём её из исходного кода и установим в систему (по желанию):

aria2c https://homepages.thm.de/~hg53/pdp11-unix/unix-v7-7.tar.gz
tar -xzvf unix-v7-7.tar.gz --strip-components=1 unix-v7-7/extract
cd extract
make
sudo cp exfs /usr/local/bin

И теперь можно запросто извлечь содержимое, к примеру, корневой папки unix.

exfs rp06-0.disk 0 pdp_root
# где 0 - это номер стартового блока диска, а pdp_root - папка, в которую извлекать

Кросскомпиляция под Windows с msvc, как проблеск здравого смысла

Многое из того, о чём я рассказал, совершенно неприменимо в современной разработке 99% времени, поэтому давайте займёмся чем-нибудь действительно "полезным", например кросскомпиляцией c++ под MS Windows 10/11 с туллчейном msvc, потому что Mingw - это во-первых скучно, а во-вторых, создаваемые им файлы часто зависят от некоторых динамических gcc библиотек, которые не всегда хочется распространять вместе с исполняемым файлом.
На этот раз, нам поможет этот проект.

Окружение и настройка

Установим компилятор и линкер llvm, которые и будут использовать msvc-тулчейн, а также msitools для успешной распаковки этого тулчейна:

brew install clang lld msitools

Загрузим и установим заголовки, библиотеки и инструменты msvc:

git clone https://github.com/mstorsjo/msvc-wine
cd msvc-wine
# скачаем и установим тулчейн в /opt/msvc, согласимся с лицензией Microsoft
sudo ./vsdownload.py --accept-license --dest /opt/msvc

Загрузка может занять около пяти минут, зависит от скорости интернет-соединения.
Откроем в текстовом редакторе файл install.sh и найдём в нём такую строку:
if [ -d HostX64 ]; then
Закомментируем все переименования папок ниже. В MacOS эти операции вызывают ошибки и не имеют смысла, так как APFS регистронезависима.
Данный блок должен будет выглядеть примерно вот так (двоеточие как nop):

Исправленная часть install.sh
if [ -d HostX64 ]; then
    # 15.x - 16.4
    : mv HostX64 Hostx64
fi
if [ -d HostARM64 ]; then
    # 17.2 - 17.3
    : mv HostARM64 Hostarm64
fi
if [ -d HostArm64 ]; then
    # 17.4
    : mv HostArm64 Hostarm64
fi
if [ -d Hostarm64/ARM64 ]; then
    # 17.2 - 17.3
    : mv Hostarm64/ARM64 Hostarm64/arm64
fi

Запустим исправленный скрипт и скопируем скрипт msvcenv-native.sh, использующийся для настройки переменных окружения:

sudo ./install.sh /opt/msvc
sudo cp msvcenv-native.sh /opt/msvc

В файл конфигурации оболочки для удобства предлагаю добавить функцию, которая будет правильно настраивать переменные окружения для разных архитектур Windows. Выглядит она примерно так:

Функция настройки окружения msvc
msvc() {
    arch="$1"
    arches="x86 x64 arm arm64"
    usage() {
        echo "Usage: msvc [arch]"
        echo "Available architectures:"
        echo "  x86   - 32-bit target"
        echo "  x64   - 64-bit target"
        echo "  arm   - ARM 32-bit target"
        echo "  arm64 - ARM64 target"
    }

    if [[ -z "$arch" || ! " $arches " =~ " $arch " ]]; then
        echo "Error: unknown or missing architecture."
        usage
        return 1
    fi
    BIN="/opt/msvc/bin/$arch"
    local setup_script="/opt/msvc/msvcenv-native.sh"
    [[ -f "$setup_script" ]] && . "$setup_script" || echo "Warning: $setup_script not found!"
    case "$arch" in
        x86)   echo "Target: Windows x86 (32-bit)" ;;
        x64)   echo "Target: Windows x64 (64-bit)" ;;
        arm)   echo "Target: Windows ARM (32-bit)" ;;
        arm64) echo "Target: Windows ARM64" ;;
    esac
}

Кросс-компиляция под Windows с LLVM Clang

Применим обновлённые настройки и попробуем собрать что-нибудь, зависящее от Windows API.
Для успешной сборки с туллчейном msvc нам понадобится установленный в начале оригинальный llvm clang (не поставляемый с XCode Apple Clang) и линкер lld. Также необходимо будет указать правильную цель сборки для использования msvc туллчейна.

msvc x64
aria2c https://raw.githubusercontent.com/microsoft/Windows-classic-samples/main/Samples/Win7Samples/begin/LearnWin32/HelloWorld/cpp/main.cpp
/opt/homebrew//opt/llvm/bin/clang++ -fuse-ld=lld --target=x86_64-w64-pc-windows-msvc main.cpp -o main.exe -luser32
# Проверим, что всё работает
wine main.exe

Итоги

Многое из того, что здесь показано, интересно лишь немногочисленным энтузиастам, но некоторые мимоходом описанные здесь инструменты действительно делают MacOS куда комфортнее для разработчика. OrbStack я сам использую каждый день для всего, что связано с виртуальными машинами Linux и контейнерами. Особо хочется отметить виртуальные машины на amd64, которые в orbstack работают через Rosetta2 и обеспечивают в несколько раз большую производительность, чем такие же виртуальные машины, эмулируемые через Qemu, которые предоставляет, например, Lima.

Спасибо, что дочитали! А если обнаружили какую-либо неточность или в вашем сценарии инструкции из статьи не сработали, обязательно расскажите об этом в комментариях.

Комментарии (1)


  1. BjLomax
    05.01.2026 09:44

    Спасибо.