experiments
December 11, 2024

Самый маленький эмулятор

Совершенно случайно и неожиданно наткнулся на самый маленький эмулятор x86 архитектуры — 4043 байт!

Внутри самый настоящий Windows 3.0, как видите даже с графикой (!) Справа в редакторе - исходный код.

Еще про эмуляцию и эмуляторы можно почитать тут и тут.

Largest small system emulator

Разумеется это работа сценеров, победитель The International Obfuscated C Code Contest (IOCCC) от 2013 года, за авторством Adrian Cable.

This entry weighs in at a magical 4043 bytes (8086 nibbles, 28,301 bits). It manages to implement most of the hardware in a 1980’s era IBM-PC using a few hundred fewer bits than the total number of transistors used to implement the original 8086 CPU.

Про IOCCC уже неоднократно упоминал, поскольку этот конкурс — бесконечный источник вдохновения для программистов, а публикуемые работы — верх инженерного мастерства.

Наша Телепорта бы точно никогда не была создана, если бы не вдумчивое и многолетнее чтение таких работ ;)

Но вернемся к эмулятору, вот как его описывает автор:

The author hereby presents, for the delectation (?) of the judges, a portable PC emulator/VM written specifically for the IOCCC which runs DOS, Windows 3.0, Excel, MS Flight Simulator, AutoCAD, Lotus 1-2-3 …

И.. все это правда:

Sim CIty (а вы не знали что это игра из 80х?)
QBasic, достаточно свежий
Microsoft Flight Simulator
Microsoft Excel, узнали?

Напоминаю, что весь исходный код эмулятора это лишь 4043 байт на Си (29 строк).

Вот список эмулируемого железа:

  • Intel 8086/186 CPU
  • 1MB RAM
  • 8072A 3.5" floppy disk controller (1.44MB/720KB)
  • Fixed disk controller (supports a single hard drive up to 528MB)
  • Hercules graphics card with 720x348 2-color graphics (64KB video RAM), and CGA 80x25 16-color text mode support
  • 8253 programmable interval timer (PIT)
  • 8259 programmable interrupt controller (PIC)
  • 8042 keyboard controller with 83-key XT-style keyboard
  • MC146818 real-time clock
  • PC speaker

Поддержка FreeBSD разумеется не заявлена:

The emulator uses the SDL graphics library for portability, and compiles for Windows, Mac OS X, Linux and probably most other 32-bit/64-bit systems too.

но все собирается и работает.

Вот так выглядит полный исходный код эмулятора:

#include "SDL.h"

#define $ for(O=9
#define CX M+=(T%3+2*!(!T*t-6))
#define x ,A=4*!T,O=t,W=h=T<3?u(Q?p:D(A+3),D(A),D(A+1)[i]+D(A+2)*g+):K(t),U=V=K(a),o?U=h,W=V:V,
#define C 8*-~L
#define Z short
#define y a(Z)Y[++O]
#define B ),a--||(
#define _ ),e--||(

#define V(I,D,E)(O=a(I)h[r])&&!(A=(D)(V=(1[E+L]<<16)+*i)/O,A-(I)A)?1[E+L]=V-O*(*E=A):H(0)
#define i(B,M)B(o){return M;}
#define R(O,M,_)(S=L?a(I Z)O:O,N=L?a(I Z)O M(f=a(I Z)_):(O M(f=a(I n)_)))
#define T(_)R(r[u(10,L=4,--)],=,_)
#define u(a,r,T)16*i[a]+(I Z)(T i[r])
#define a(_)*(_*)&
#define L(_)M(W,_,U)

#define M(S,F,T)R(r[S],F,r[T])
#define A(_)(i[L=4]+=2,R(_,=,r[u(10,4,-2+)]))
#define c(R,T)(1[u=19,L+T]=(N=a(R)h[r]*(R)*T)>>16,*i=N,G(F(N-(R)N)))
#define h(_)(1&(L?a(Z)_:_)>>C-1)
#define I unsigned
#define n char
#define e(_)v(F(40[L(_##=40[E]+),E]&N==S|_ N<_(int)S))

I n t,e,l[80186],*E,m,u,L,a,T,o,r[1<<21],X,*Y,b,Q,R;I Z*i,M,p,q=3;I*localtime(),f,S,kb,h,W,U,c,g,d,V,A;N,O,P=983040,j[5];SDL_Surface*k;i(F,40[E]=!!o)i(z,42[E]=!!o)i(D,r[a(I)E[259+4*o]+O])i(w,i[o]+=~(-2*47[E])*~L)i(v,G(N-S&&1&(40[z((f^=S^N)&16),E]^f>>C-1)))J(){V=61442;$;O--;)V+=40[E+O]<<D(25);}i(H,(46[u=76,J(),T(V),T(9[i]),T(M),M(P+18,=,4*o+2),R(M,=,r[4*o]),E]=0))s(o){$;O--;)40[E+O]=1&&1<<D(25)&o;}i(BP,(*i+=262*o*z(F((*E&15)>9|42[E])),*E&=15))i(SP,(w(7),R&&--1[i]&&o?R++,Q&&Q++,M--:0))DX(){$,O*=27840;O--;)O[(I*)k->pixels]=-!!(1<<7-O%8&r[O/2880*90+O%720/8+(88+952[l]/128*4+O/720%4<<13)]);SDL_Flip(k);}main(BX,nE)n**nE;{9[i=E=r+P]=P>>4;$;q;)j[--q]=*++nE?open(*nE,32898):0;read(2[a(I)*i=*j?lseek(*j,0,2)>>9:0,j],E+(M=256),P);$;Y=r+16*9[i]+M,Y-r;Q|R||kb&46[E]&&KB)--64[T=1[O=32[L=(X=*Y&7)&1,o=X/2&1,l]=0,t=(c=y)&7,a=c/8&7,Y]>>6,g=~-T?y:(n)y,d=BX=y,l],!T*t-6&&T-2?T-1?d=g:0:(d=y),Q&&Q--,R&&R--x(O=*Y,O=u=D(51),e=D(8),m=D(14)_ O=*Y/2&7,M+=(n)c*(L^(D(m)[E]|D(22)[E]|D(23)[E]^D(24)[E]))_ L=*Y&8,R(K(X)[r],=,c)_ L=e+=3,o=0,a=X x a=m _ T(X[i])_ A(X[i])_ a<2?M(U,+=1-2*a+,P+24),v(f=1),G(S+1-a==1<<C-1),u=u&4?19:57:a-6?CX+2,a-3||T(9[i]),a&2&&T(M),a&1&&M(P+18,=,U+2),R(M,=,U[r]),u=67:T(h[r])_(W=U B u=m,M-=~L,R(W[r],&,d)B 0 B L(=~)B L(=-),S=0,u=22,F(N>S)B L?c(I Z,i):c(I n,E)B/**/L?c(Z,i):c(n,E)B L?V(I Z,I,i):V(I n,I Z,E)B L?V(Z,int,i):V(n,Z,E))_++e,h=P,d=c,T=3,a=m,M--_++e,13[W=h,i]=(o|=!L)?(n)d:d,U=P+26,M-=~!o,u=17+(m=a)_(a=m B L(+=),F(N<S)B L(|=)B e(+)B e(-)B L(&=)B L(-=),F(N>S)B L(^=)B L(-),F(N>S)B L(=))_!L?L=a+=8 x L(=):!o?Q=1,R(r[p=m x V],=,h):A(h[r])_ T=a=0,t=6,g=c x M(U,=,W)_(A=h(h[r]),V=m?++M,(n)g:o?31&2[E]:1)&&(a<4?V%=a/2+C,R(A,=,h[r]):0,a&1?R(h[r],>>=,V):R(h[r],<<=,V),a>3?u=19:0,a<5?0:F(S>>V-1&1)B R(h[r],+=,A>>C-V),G(h(N)^F(N&1))B A&=(1<<V)-1,R(h[r],+=,A<<C-V),G(h(N*2)^F(h(N)))B R(h[r],+=(40[E]<<V-1)+,A>>1+C-V),G(h(N)^F(A&1<<C-V))B R(h[r],+=(40[E]<<C-V)+,A<<1+C-V),F(A&1<<V-1),G(h(N)^h(N*2))B G(h(N)^F(h(S<<V-1)))B G(h(S))B 0 B V<C||F(A),G(0),R(h[r],+=,A*=~((1<<C)-1>>V)))_(V=!!--1[a=X,i]B V&=!m[E]B V&=m[E]B 0 B V=!++1[i]),M+=V*(n)c _ M+=3-o,L?0:o?9[M=0,i]=BX:T(M),M+=o*L?(n)c:c _ M(U,&,W)_ L=e+=8,W=P,U=K(X)_!R||1[i]?M(m<2?u(8,7,):P,=,m&1?P:u(Q?p:11,6,)),m&1||w(6),m&2||SP(1):0 _!R||1[i]?M(m?P:u(Q?p:11,6,),-,u(8,7,)),43[u=92,E]=!N,F(N>S),m||w(6),SP(!N==b):0 _ o=L,A(M),m&&A(9[i]),m&2?s(A(V)):o||(4[i]+=c)_ R(U[r],=,d)_ 986[l]^=9,R(*E,=,l[m?2[i]:(n)c])_ R(l[m?2[i]:(n)c],=,*E)_ R=2,b=L,Q&&Q++_ W-U?L(^=),M(U,^=,W),L(^=):0 _ T(m[i])_ A(m[i])_ Q=2,p=m,R&&R++_ L=0,O=*E,F(D(m+=3*42[E]+6*40[E])),z(D(1+m)),N=*E=D(m-1)_ N=BP(m-1)_ 1[E]=-h(*E)_ 2[i]=-h(*i)_ 9[T(9[i]),T(M+5),i]=BX,M=c _ J(),T(V)_ s(A(V))_ J(),s((V&~m)+1[E])_ J(),1[E]=V _ L=o=1 x L(=),M(P+m,=,h+2)_++M,H(3)_ M+=2,H(c&m)_++M,m[E]&&H(4)_(c&=m)?1[E]=*E/c,N=*E%=c:H(0)_*i=N=m&E[L=0]+c*1[E]_*E=-m[E]_*E=r[u(Q?p:m,3,*E+)]_ m[E]^=1 _ E[m/2]=m&1 _ R(*E,&,c)_(a=c B write(1,E,1)B time(j+3),memcpy(r+u(8,3,),localtime(j+3),m)),a<2?*E=~lseek(O=4[E][j],a(I)5[i]<<9,0)?((I(*)())(a?write:read))(O,r+u(8,3,),*i):0:0),O=u,D(16)?v(0):D(17)&&G(F(0)),CX*D(20)+D(18)-D(19)*~!!L,D(15)?O=m=N,41[43[44[E]=h(N),E]=!N,E]=D(50):0,!++q?kb=1,*l?SDL_PumpEvents(),k=k?k:SDL_SetVideoMode(720,348,32,0),DX():k?SDL_Quit(),k=0:0:0;}i(G,48[E]=o)i(K,P+(L?2*o:2*o+o/4&7))

И нет, даже пытаться не буду это разбирать.

На самом деле все уже давно разобрали и вот тут лежит читаемая и детально откомментированная версия.

Если вдруг интересно как это работает.

Еще тут лежит образ диска в 40Мб, на котором и установлен весь софт из демки, включая Windows 3.0

Сборка

Как это ни странно, но оригинальный проект 2013 года, с обфрускацией для IOCCC собирается и запускается без каких-либо проблем:

Сборка производилась на FreeBSD, стандартным make.

Запуск осуществляется с помощью скрипта runme, по-умолчанию внутри запускается аж современный FreeDOS!

Но вот почищенная и осовремененная версия уже падает при сборке с ошибкой:

ld: error: undefined symbol: ftime
>>> referenced by 8086tiny.c

Происходит это потому, что вызов ftime успел устареть:

 This interface is obsoleted by gettimeofday(2).

а его использование ныне требует включения специального ключа -lcompat

Который я и добавил в Makefile:

И.. немедленно обломался, потому что в FreeBSD версии 14.2, вышедшей на прошлой неделе случилось вот такое:

 lib/libcompat/Makefile                 | 7 +------
 lib/libutil/Makefile                   | 6 +++---
 lib/{libcompat/4.1 => libutil}/ftime.3 | 4 ++--
 lib/{libcompat/4.1 => libutil}/ftime.c | 2 ++
 sys/sys/timeb.h                        | 2 +-
 5 files changed, 9 insertions(+), 12 deletions(-)

Так что вместо lcompat теперь надо использовать -lutil:

Либо полностью отказываться от использования устаревшего ftime в коде, что конечно предпочтительно но только я нихрена не автор актуально лишь для FreeBSD, например в MacOS никаких проблем с ftime нет.

BIOS

В корне проекта лежит файл bios, это уже собранный образ биоса, исходник которого находится в каталоге bios_source:

Like a real PC, the emulator needs a BIOS to do anything useful. Here we use a custom BIOS, written from scratch specifically for the emulator

Собирается биос с помощью nasm:

nasm bios.asm

Самое интересное, связанное с эмуляцией оборудования это наверное работа с клавиатурой:

The emulator simulates an XT-style keyboard controlled by an Intel 8042 chip on I/O port 0x60, generating IRQ1 and then interrupt 9 on each keypress. This is harder than it sounds because a real 8042 returns scan codes rather than the ASCII characters which the C standard I/O functions return. Rather than make the emulator less portable and use ioctl or platform-dependent equivalents to obtain real scan codes from the keyboard, the emulator BIOS does the reverse of a real PC BIOS and converts ASCII characters to scancodes, simulating press/release of the modifier keys (e.g. shift) as necessary to work like a “real” keyboard. The OS (DOS/Windows) then converts them back to ASCII characters and normally this process works seamlessly (although don’t be surprised if there are issues, for example, with non-QWERTY e.g. international keyboards).

Чистый, 100% грязный хак, в лучших традициях.

На сладкое покажу что еще можно запустить c помощью этого эмулятора.

ELKS

Врядли вы знали что такое на свете вообще существует (я не знал):

ELKS is a project providing a Linux-like OS for systems based on the Intel IA16 architecture (16-bit processors: 8086, 8088, 80188, 80186, 80286, NEC V20, V30 and compatibles). Such systems are ancient computers (IBM-PC XT / AT and clones) as well as more recent SBCs, SoCs, and FPGAs. ELKS supports networking and installation to HDD using both MINIX and FAT file systems.

Официальная история Linux начинается с 386го, на котором Торвальдс и начинал разработку, поэтому поддержка всего что проще 386го это бекпорт.

Чтобы вам стало совсем страшно, вот вам снимок ELKS, работающего на машине одного со мной возраста:

Не стал заморачиваться сборкой еще и ELKS с нуля (про нее все равно будет отдельная статья), взяв готовый образ 1.44 дискеты:

Emulates a 3.5" high-density floppy drive. Can read, write and format 1.44MB disks (18 sectors per track, 2 heads) and 720KB disks (9 sectors per track, 2 heads).

Вот так это выглядит в работе:

QNX2

Существует интересный патч от небезизвестного neozeed, с которым по идее можно запускать QNX2 в этом маленьком эмуляторе:

К сожалению ни один из найденных образов дисков QNX у меня с этим патчем так и не запустился.