Tutorial: 4k Intros in Linux

About two years ago I started trying to create 4k Intros in Linux. As I noticed, there isn't much useful documentation available, so I started writing a Tutorial for it.

In the first part, we'll learn how to use a gzexe-like script to pack executables much better than with runtime-packing-tools like upx. In part 2, I'll talk about using gcc for creating small intros, In the third part we'll use assembler, the fourth is about writing our own ELF-headers. Part 5 contains some generic tips about writing 4ks.

Part 1: Packing the executable

UPX is the only "real" executable-packer in linux known to me. However, it won't help us much, because on very small files, it just says

CantPackException: file is too small

So we need something else. As part of the gzip-package, you'll have the tool gzexe. What it does is simple: It creates a small script and attaches the executable gzip-packed. The script unpacks it's own attached part to /tmp and runs it.

The gzexe-created executable-script contains much bloat we don't need. Here is an optimized version of it:

dd bs=1 skip=61<$0|gunzip>/tmp/C;chmod +x /tmp/C;/tmp/C;exit

dd will extract everything behind the script, pipes it to gunzip and writes it to /tmp/C. chmod makes it executable and then we run it. You can remove the exit-command at the end, however this will result in a lot of garbage on the console after your executable quits./p>

You can download the script here. To use it, just copy it to a filename, let's say 4k and execute

gzip -cn9 [yourexecutable] >> 4k

Parameter -c avoids saving of filename and timestamp in the packed file and -n9 enables best compression.

Part 2: Using gcc

Okay, now we've learned how to pack our executable, but we still don't know how to create executables small enough for 4ks. If we use normal gcc to compile a simple, empty program, we already have a filesize of about 7 kb, far too much to create something useful in 4k.

As we want to have nice graphics, we need OpenGL. We won't use libGLU to keep our executable small.. To initialize our OpenGL-Framework, we'll use SDL, because it's the simplest way and SDL should be available on pretty much every usual linux system out there.

Here is a simple source, just displaying a triangle and waiting for a key to be pressed:

#include "GL/gl.h"
#include "SDL/SDL.h"

int main()
{
  SDL_Event event;

  SDL_SetVideoMode(640,480,0,SDL_OPENGL|SDL_FULLSCREEN);
  SDL_ShowCursor(SDL_DISABLE);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.33,1.33,-1,1,1.5,100);
  glMatrixMode(GL_MODELVIEW);
  glEnable(GL_DEPTH_TEST);
  glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);

  glLoadIdentity();
  glBegin(GL_TRIANGLES);
  glVertex3i(1,1,-10);
  glVertex3i(1,-1,-10);
  glVertex3i(-1,1,-10);
  glEnd();
  SDL_GL_SwapBuffers();
  
  do
  {
    SDL_PollEvent(&event);
  } while (event.type!=SDL_KEYDOWN);
  SDL_Quit();
}

Usually you initialize SDL with SDL_Init, but it also works if we leave this out. If you really have to save space, you can remove the SDL_ShowCursor command and tell the orgas to move the mouse away.

Now, we compile it using size-optimization and no frame-pointers (as we don't want to debug our intro). We'll strip the executable and also remove the .comment-section and the .gnu.version-section (useless information in an executable).

gcc -Os -fomit-frame-pointer [filename].c -o [filename]
strip -s -R .comment -R .gnu.version [filename]

Now we have about 3800 bytes. Still a bit too much. Now, there is a tool called sstrip, part of the package ELFkickers. With

sstrip [executable]

we'll save about 1k. It's becoming better.

By default gcc places some startup- and end-code of the glibc into our executable. We really don't want this in a 4k-intro. So, our next step is to manually compile our file. But first, we have to change some things in our code. Instead of int main() (which is only a function called by the glibc-code), we'll use void _start(). Now, to replace the glibc-end-code, we need a short asm-part to exit our program. The new code looks like this:

#include "GL/gl.h"
#include "SDL/SDL.h"

void _start()
{

[...]

  asm ( \
  "movl $1,%eax\n" \
  "xor %ebx,%ebx\n" \
  "int $128\n" \
  );
}

Now, to compile, strip, sstrip and pack:

gcc -Os -fomit-frame-pointer -c [filename].c
ld -dynamic-linker /lib/ld-linux.so.2 [filename].o /usr/lib/libSDL.so /usr/lib/libGL.so -o [filename]
strip -s -R .comment -R .gnu.version [filename]
sstrip [filename]
cp unpack.header [packed_filename]
gzip -cn9 [filename] >> [packed_filename]

967 bytes and we already have some basic OpenGL-code in it. That's a good point to start.

Now, for a stunning 4k, we need sound. We'll use OSS, as it can be used just with file-operations and doesn't need any libraries. Although ALSA is quite the default now on most linux-systems, the OSS-emulation is enabled by default, so our sound will also work on ALSA-systems.

For testing, we'll add some crappy sound to our code, just a sawtooth wave looped:

#include "sys/soundcard.h"
#include "fcntl.h"
#include "sys/ioctl.h"
#include "unistd.h"

[...]

  int audio_fd,i;
  short audio_buffer[4096];

  audio_fd = open("/dev/dsp", O_WRONLY, 0);
  i=AFMT_S16_LE;ioctl(audio_fd,SNDCTL_DSP_SETFMT,&i);
  i=1;ioctl(audio_fd,SNDCTL_DSP_CHANNELS,&i);
  i=11024;ioctl(audio_fd,SNDCTL_DSP_SPEED,&i);

  for (i=0;i<4096;i++)
  {
    audio_buffer[i]=i<<8;
  }

[...]

  do
  {
    ioctl(audio_fd,SNDCTL_DSP_SYNC);
    write(audio_fd,audio_buffer,8192);
    SDL_PollEvent(&event);
  } while (event.type!=SDL_KEYDOWN);

[...]

For real sound, you'll need to use the timing-routines of OSS. I suggest you read the official OSS-documentation.

That's it, now you know the basics how to create a linux-4k in c. To be complete, here is our full example-source, which you can also download:

#include "GL/gl.h"
#include "SDL/SDL.h"
#include "sys/soundcard.h"
#include "fcntl.h"
#include "sys/ioctl.h"
#include "unistd.h"

void _start()
{
  SDL_Event event;

  int audio_fd,i;
  short audio_buffer[4096];

  audio_fd = open("/dev/dsp", O_WRONLY, 0);
  i=AFMT_S16_LE;ioctl(audio_fd,SNDCTL_DSP_SETFMT,&i);
  i=1;ioctl(audio_fd,SNDCTL_DSP_CHANNELS,&i);
  i=11024;ioctl(audio_fd,SNDCTL_DSP_SPEED,&i);

  for (i=0;i<4096;i++)
  {
    audio_buffer[i]=i<<8;
  }

  SDL_SetVideoMode(640,480,0,SDL_OPENGL|SDL_FULLSCREEN);
  SDL_ShowCursor(SDL_DISABLE);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.33,1.33,-1,1,1.5,100);
  glMatrixMode(GL_MODELVIEW);
  glEnable(GL_DEPTH_TEST);
  glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);

  glLoadIdentity();
  glBegin(GL_TRIANGLES);
  glVertex3i(1,1,-10);
  glVertex3i(1,-1,-10);
  glVertex3i(-1,1,-10);
  glEnd();
  SDL_GL_SwapBuffers();

  do
  {
    ioctl(audio_fd,SNDCTL_DSP_SYNC);
    write(audio_fd,audio_buffer,8192);
    SDL_PollEvent(&event);
  } while (event.type!=SDL_KEYDOWN);
  SDL_Quit();

  asm ( \
  "movl $1,%eax\n" \
  "xor %ebx,%ebx\n" \
  "int $128\n" \
  );
}