Рисование символа в VGA-памяти с встроенной сборкой GNU C

Я изучаю некоторые низкоуровневые VGA-программы в DOS с C и встроенной сборкой. Сейчас я пытаюсь создать функцию, которая печатает символ на экране.

Это мой код:

//This is the characters BITMAPS
uint8_t characters[464] = {
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x20,0x20,0x00,0x20,0x00,0x50,
  0x50,0x00,0x00,0x00,0x00,0x00,0x50,0xf8,0x50,0x50,0xf8,0x50,0x00,0x20,0xf8,0xa0,
  0xf8,0x28,0xf8,0x00,0xc8,0xd0,0x20,0x20,0x58,0x98,0x00,0x40,0xa0,0x40,0xa8,0x90,
  0x68,0x00,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x20,0x40,0x40,0x40,0x40,0x20,0x00,
  0x20,0x10,0x10,0x10,0x10,0x20,0x00,0x50,0x20,0xf8,0x20,0x50,0x00,0x00,0x20,0x20,
  0xf8,0x20,0x20,0x00,0x00,0x00,0x00,0x00,0x60,0x20,0x40,0x00,0x00,0x00,0xf8,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00,0x08,0x10,0x20,0x40,0x80,
  0x00,0x70,0x88,0x98,0xa8,0xc8,0x70,0x00,0x20,0x60,0x20,0x20,0x20,0x70,0x00,0x70,
  0x88,0x08,0x70,0x80,0xf8,0x00,0xf8,0x10,0x30,0x08,0x88,0x70,0x00,0x20,0x40,0x90,
  0x90,0xf8,0x10,0x00,0xf8,0x80,0xf0,0x08,0x88,0x70,0x00,0x70,0x80,0xf0,0x88,0x88,
  0x70,0x00,0xf8,0x08,0x10,0x20,0x20,0x20,0x00,0x70,0x88,0x70,0x88,0x88,0x70,0x00,
  0x70,0x88,0x88,0x78,0x08,0x70,0x00,0x30,0x30,0x00,0x00,0x30,0x30,0x00,0x30,0x30,
  0x00,0x30,0x10,0x20,0x00,0x00,0x10,0x20,0x40,0x20,0x10,0x00,0x00,0xf8,0x00,0xf8,
  0x00,0x00,0x00,0x00,0x20,0x10,0x08,0x10,0x20,0x00,0x70,0x88,0x10,0x20,0x00,0x20,
  0x00,0x70,0x90,0xa8,0xb8,0x80,0x70,0x00,0x70,0x88,0x88,0xf8,0x88,0x88,0x00,0xf0,
  0x88,0xf0,0x88,0x88,0xf0,0x00,0x70,0x88,0x80,0x80,0x88,0x70,0x00,0xe0,0x90,0x88,
  0x88,0x90,0xe0,0x00,0xf8,0x80,0xf0,0x80,0x80,0xf8,0x00,0xf8,0x80,0xf0,0x80,0x80,
  0x80,0x00,0x70,0x88,0x80,0x98,0x88,0x70,0x00,0x88,0x88,0xf8,0x88,0x88,0x88,0x00,
  0x70,0x20,0x20,0x20,0x20,0x70,0x00,0x10,0x10,0x10,0x10,0x90,0x60,0x00,0x90,0xa0,
  0xc0,0xa0,0x90,0x88,0x00,0x80,0x80,0x80,0x80,0x80,0xf8,0x00,0x88,0xd8,0xa8,0x88,
  0x88,0x88,0x00,0x88,0xc8,0xa8,0x98,0x88,0x88,0x00,0x70,0x88,0x88,0x88,0x88,0x70,
  0x00,0xf0,0x88,0x88,0xf0,0x80,0x80,0x00,0x70,0x88,0x88,0xa8,0x98,0x70,0x00,0xf0,
  0x88,0x88,0xf0,0x90,0x88,0x00,0x70,0x80,0x70,0x08,0x88,0x70,0x00,0xf8,0x20,0x20,
  0x20,0x20,0x20,0x00,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x88,0x88,0x88,0x88,0x50,
  0x20,0x00,0x88,0x88,0x88,0xa8,0xa8,0x50,0x00,0x88,0x50,0x20,0x20,0x50,0x88,0x00,
  0x88,0x50,0x20,0x20,0x20,0x20,0x00,0xf8,0x10,0x20,0x40,0x80,0xf8,0x00,0x60,0x40,
  0x40,0x40,0x40,0x60,0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x30,0x10,0x10,0x10,
  0x10,0x30,0x00,0x20,0x50,0x88,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf8,
  0x00,0xf8,0xf8,0xf8,0xf8,0xf8,0xf8};
/**************************************************************************
 *  put_char                                                              *
 *     Print char                                                         *
 **************************************************************************/
void put_char(int x ,int y,int ascii_char ,byte color){

    __asm__(
        "push %si\n\t"
        "push %di\n\t"
        "push %cx\n\t"
        "mov color,%dl\n\t"   //test color
        "mov ascii_char,%al\n\t"  //test char
        "sub $32,%al\n\t"
        "mov $7,%ah\n\t"
        "mul %ah\n\t"
        "lea $characters,%si\n\t"
        "add %ax,%si\n\t"
        "mov $7,%cl\n\t"
        "0:\n\t"
        "segCS %lodsb\n\t"   
        "mov $6,%ch\n\t"
        "1:\n\t"    
        "shl $1,%al\n\t"
        "jnc 2f\n\t"
        "mov %dl,%ES:(%di)\n\t"
        "2:\n\t"
        "inc %di\n\t"
        "dec %ch\n\t"
        "jnz 1b\n\t"
        "add $320-6,%di\n\t"
        "dec %cl\n\t"
        "jnz  0b\n\t"
        "pop %cx\n\t"
        "pop %di\n\t"
        "pop %si\n\t"
        "retn"

    );


}

Я направляюсь из этой серии руководств, написанных в PASCAL: http://www.joco.homeserver.hu/vgalessons/lesson8.html.

Я изменил синтаксис сборки в соответствии с компилятором gcc, но я все еще получаю следующие ошибки:

Operand mismatch type for 'lea'
No such instruction 'segcs lodsb'
No such instruction 'retn'

EDIT:

Я работаю над улучшением своего кода и по крайней мере теперь вижу что-то на экране. Вот мой обновленный код:

/**************************************************************************
 *  put_char                                                              *
 *     Print char                                                         *
 **************************************************************************/
void put_char(int x,int y){
    int char_offset;
    int l,i,j,h,offset;
    j,h,l,i=0;
    offset = (y<<8) + (y<<6) + x;               
    __asm__(

        "movl _VGA, %%ebx;" // VGA memory pointer   
        "addl %%ebx,%%edi;"  //%di points to screen


        "mov _ascii_char,%%al;"
        "sub $32,%%al;"
        "mov $7,%%ah;"
        "mul %%ah;"

        "lea _characters,%%si;"
        "add %%ax,%%si;"   //SI point to bitmap

        "mov $7,%%cl;"

        "0:;"
            "lodsb %%cs:(%%si);"   //load next byte of bitmap 

            "mov $6,%%ch;"
        "1:;"   
            "shl $1,%%al;"
            "jnc 2f;"
            "movb %%dl,(%%edi);"  //plot the pixel
        "2:\n\t"
            "incl %%edi;"
            "dec %%ch;"
            "jnz 1b;"
            "addl $320-6,%%edi;"
            "dec %%cl;"
            "jnz  0b;"


        :  "=D" (offset)
        : "d" (current_color)

    );


}

Если вы видите изображение выше, я пытался написать букву "S". Результатом являются зеленые пиксели, которые вы видите в верхней левой части экрана. Независимо от того, какие x и y я даю functon, он всегда отображает пиксели в том же месте.

введите описание изображения здесь

Может ли кто-нибудь помочь мне исправить свой код?

Ответы

Ответ 1

См. ниже анализ некоторых вещей, которые специально не соответствуют вашей функции put_char, и версии, которые могут работать. (Я не уверен в переопределении сегмента %cs, но кроме этого он должен делать то, что вы намерены).


Обучение DOS и 16-бит asm - не лучший способ узнать asm

Прежде всего, DOS и 16-разрядные x86 полностью устарели, и их нелегко изучить, чем обычные 64-разрядные x86. Даже 32-разрядный x86 является устаревшим, но все еще широко используется в мире Windows.

32-разрядный и 64-разрядный код не должны заботиться о большом количестве 16-разрядных ограничений/осложнений, таких как сегменты или ограниченном выборе регистра в режимах адресации. Некоторые современные системы используют переопределение сегментов для локального хранилища потоков, но изучение того, как использовать сегменты в 16-битном коде, едва ли связано с этим.

Одним из основных преимуществ знания asm является отладка/профилирование/оптимизация реальных программ. Если вы хотите понять, как писать C или другой высокоуровневый код который может (и на самом деле) компилироваться в эффективный asm, вы, вероятно, будете просмотр выходных данных компилятора. Это будет 64-разрядная (или 32-разрядная). (например, см. Matt Godbolt CppCon2017 talk: "Что мой компилятор сделал для меня в последнее время? Откручивание крышки компилятора" , который имеет отличное введение в чтение x86 asm для начинающих, а также для вывода компилятора).

Знание Asm полезно при просмотре результатов счетчика производительности, аннотируя разборку вашего двоичного файла (perf stat ./a.out& perf report -Mintel: см. Chandler Carruth CppCon2015 говорит: "Тюнинг С++: тесты, процессоры и компиляторы! Oh My!" ). Агрессивные оптимизаторы компилятора означают, что просмотр количества циклов/кеш-промаха/стойла на исходную строку намного менее информативен, чем для каждой команды.

Кроме того, для вашей программы на самом деле что-либо делать, она должна либо напрямую обращаться к аппаратным средствам, либо делать системные вызовы. Изучение систем DOS для доступа к файлам и ввода пользователем - полная трата времени (за исключением ответа на постоянный поток SO-вопросов о том, как читать и печатать многозначные числа в 16-битном коде). Они сильно отличаются от API в современных ОС. Разработка новых приложений DOS не пригодится, поэтому вам нужно будет изучить еще один API (а также ABI), когда вы дойдете до стадии выполнения чего-либо с вашим знанием asm.

Изучение asm на симуляторе 8086 еще более ограничивает: 186, 286 и 386 добавили много удобных инструкций, таких как imul ecx, 15, что делает ax менее "особенным". Ограничение себя только инструкциями, которые работают на 8086, означает, что вы поймете "плохие" способы сделать что-то. Другие большие - movzx/movsx, сдвиг на непосредственный счет (кроме 1) и push immediate. Помимо производительности, также легче писать код, когда они доступны, потому что вам не нужно писать цикл для переключения более чем на 1 бит.


Предложения для лучших способов научить себя asm

Я в основном узнал asm из чтения компилятора, а затем внес небольшие изменения. Я не пытался писать материал в asm, когда я действительно не понимал вещи, но если вы будете учиться быстро (вместо того, чтобы просто развивать понимание при отладке/профилировании C), вам, вероятно, нужно проверить свое понимание написав собственный код. Вам нужно понять основы, что есть 8 или 16 целочисленных регистров + флаги и указатель инструкции, и каждая инструкция делает четко определенную модификацию текущего архитектурного состояния машины. (См. Руководство по инсталляции Intel insn для полного описания каждой инструкции (ссылки в wiki, а также гораздо больше хороших вещей).

Вы можете начать с простых вещей, таких как запись одной функции в asm, как часть более крупной программы. Понимание типа asm, необходимого для выполнения системных вызовов, полезно, но в реальных программах это обычно полезно только для ручной записи asm для внутренних циклов, которые не связаны с системными вызовами. Это занимает много времени, чтобы написать asm для чтения ввода и печати результатов, поэтому я бы предложил сделать эту часть на C. Убедитесь, что вы прочитали вывод компилятора и поняли, что происходит, и разницу между целым числом и строкой, и что strtol и printf делать, даже если вы сами не пишете их.

Как только вы подумаете, что понимаете достаточно оснований, найдите функцию в какой-либо программе, с которой вы знакомы и/или заинтересованы, и посмотрите, можете ли вы побить компилятор и сохранить инструкции (или использовать более быстрые инструкции). Или реализуйте его самостоятельно, не используя выход компилятора в качестве отправной точки, в зависимости от того, что вы найдете более интересным. Этот ответ может быть интересным, хотя в фокусе находилось обнаружение источника C, который заставил компилятор создать оптимальную ASM.

Как попытаться решить свои проблемы (прежде чем задавать вопрос SO)

Есть много вопросов SO от людей, которые спрашивают: "Как мне делать X в asm", и ответ обычно "такой же, как и на C". Не поймите так, что вы не знаете, как программировать. Выясните, что должно произойти с данными, на которых работает функция, а затем выясните, как это сделать в asm. Если вы застряли и должны задать вопрос, у вас должна быть большая часть рабочей реализации, и только одна часть, которую вы не знаете, какие инструкции следует использовать для одного шага.

Вы должны сделать это с 32 или 64 бит x86. Я бы предложил 64-битный, поскольку ABI лучше, но 32-битные функции заставят вас больше использовать стек. Таким образом, это может помочь вам понять, как команда call помещает обратный адрес в стек и где аргументы, вызываемые на самом деле, вызываются после этого. (Кажется, это то, к чему вы пытались избежать, используя inline asm).


Аппаратное обеспечение программирования прямое, но не общепринятое умение

Обучение графике путем непосредственного изменения видеопамяти не полезно, кроме как удовлетворить любопытство о том, как компьютеры работали. Вы не можете использовать это знание ни для чего. Современные графические API существуют, чтобы позволить нескольким программам рисовать в своих областях экрана и допускать косвенность (например, рисовать текстуру вместо экрана напрямую, так что 3D-перетаскивание Alt-Tab может выглядеть фантастически). Там слишком много причин перечислять здесь, чтобы не рисовать непосредственно на видеопамяти.

Возможно использование рисунка в буфере pixmap, а затем использование графического API для его копирования на экран. Тем не менее, растровая графика вообще более или менее устарела, если только вы не создаете изображения для PNG или JPEG или что-то в этом роде (например, оптимизируйте преобразование гистограмм в график рассеяния во внутреннем коде веб-службы). Современные графические API абстрагируют разрешение, поэтому ваше приложение может рисовать вещи с разумным размером независимо от того, насколько большой размер каждого пикселя. (маленький, но чрезвычайно высокий экран rez против большого телевизора при низких значениях).

Приятно писать в память и видеть что-то на экране. Или еще лучше, подключите светодиоды (с малыми резисторами) к битам данных на параллельном порту и запустите инструкцию outb, чтобы включить/выключить их. Я делал это в своей системе Linux давным-давно. Я сделал небольшую программу-оболочку, которая использовала iopl(2) и inline asm, и запускала ее как root. Вероятно, вы можете сделать подобное в Windows. Вам не нужен DOS или 16-битный код, чтобы ваши ноги были мокрыми, разговаривая с оборудованием.

in/out, а нормальные нагрузки/хранилища в IO с памятью и DMA - это то, как настоящие драйверы разговаривают с оборудованием, в том числе с гораздо более сложными задачами, чем параллельные порты. Приятно знать, как работает ваше "действительно" оборудование, но тратить на него только время, если вы действительно заинтересованы или хотите писать драйверы. Дерево источников Linux включает в себя драйверы для лодок аппаратного обеспечения и часто хорошо комментируется, поэтому, если вам нравится читать код, а также писать код, это еще один способ понять, что читают драйверы, когда они разговаривают с оборудованием.

Как правило, хорошо иметь представление о том, как все работает под капотом. Если вы хотите узнать о том, как графика использовалась для работы веками назад (с текстовым режимом VGA и байтами цвета/атрибута), тогда обязательно, сходите гайки. Просто имейте в виду, что современные ОС не используют текстовый режим VGA, поэтому вы даже не узнаете, что происходит под капотом на современных компьютерах.

Многие люди наслаждаются https://retrocomputing.stackexchange.com/, переживая более простое время, когда компьютеры были менее сложными и не могли поддерживать столько слоев абстракции. Просто имейте в виду, что вы делаете. Я мог бы стать хорошим шагом в обучении написанию драйверов для современного оборудования, если вы уверены, что вы хотите понять asm/hardware.


Inline asm

Вы используете совершенно неправильный подход к использованию встроенного ASM. Кажется, вы хотите написать целые функции в asm, поэтому вам нужно просто сделать это. например введите код в asmfuncs.S или что-то в этом роде. Используйте .S, если вы хотите использовать синтаксис GNU/AT & T; или используйте .asm, если вы хотите использовать синтаксис Intel/NASM/YASM (который я бы рекомендовал, поскольку в официальных руководствах используется синтаксис Intel. См. x86 wiki для руководств и руководств.)

GNU inline asm - самый трудный способ узнать ASM. Вы должны понимать все, что делает ваш asm, и что компилятор должен знать об этом. Очень трудно все исправить. Например, в вашем редактировании этот блок inline asm изменяет многие регистры, которые вы не перечислили как сбитые, включая %ebx, который является регистром с сохранением вызова (так что это сломано, даже если эта функция не включена). По крайней мере, вы вытащили ret, поэтому вещи не будут разрываться так же эффектно, когда компилятор включит эту функцию в цикл, который ее вызывает. Если это звучит очень сложно, потому что это так, и часть почему , вы не должны использовать встроенный asm для изучения asm.

Этот ответ на аналогичный вопрос, связанный с неправильным использованием inline asm при попытке изучить asm в первую очередь, содержит больше ссылок о встроенном asm и том, как его использовать.


Как работает этот беспорядок, возможно

Эта часть может быть отдельным ответом, но я оставлю это вместе.

Помимо того, что весь ваш подход представляет собой плохую идею, существует хотя бы одна конкретная проблема с вашей функцией put_char: вы используете offset как операнд только для вывода. gcc довольно счастливо компилирует всю вашу функцию в одну инструкцию ret, потому что оператор asm не volatile, и его вывод не используется. (Встрочные операторы asm без выходов считаются volatile.)

I поместите свою функцию на godbolt, чтобы я мог посмотреть, какая сборка создает вокруг него компилятор. Эта ссылка относится к исправленной, возможно, рабочей версии, с правильно объявленными клоберами, комментариями, очистками и оптимизациями. См. Ниже тот же код, если эта внешняя ссылка когда-либо ломается.

Я использовал gcc 5.3 с опцией -m16, которая отличается от использования реального 16-битного компилятора. Он по-прежнему выполняет все 32-битный способ (используя 32-разрядные адреса, 32-битные int s и 32-битные функции args в стеке), но сообщает ассемблеру, что CPU будет в 16-битном режиме, поэтому он будет знать, когда испускать размер операнда и префиксы размера адреса.

Даже если вы скомпилируете свою исходную версию с помощью -O0, компилятор вычисляет offset = (y<<8) + (y<<6) + x;, но не помещает его в %edi, потому что вы этого не просили. Указание его как другого входного операнда работало бы. После встроенного asm он сохраняет %edi в -12(%ebp), где offset живет.


Другие вещи неверны с put_char:

Вы передаете 2 вещи (ascii_char и current_color) в свою функцию через глобальные переменные вместо аргументов функции. Як, это отвратительно. VGA и characters являются константами, поэтому загрузка их из глобальных переменных выглядит не так уж плохо. Запись в asm означает, что вы должны игнорировать хорошие методы кодирования только тогда, когда это помогает достижению разумной суммы. Поскольку вызывающему абоненту, вероятно, приходилось хранить эти значения в глобалах, вы ничего не сохраняете по сравнению с вызывающим, хранящим их в стеке как функции args. И для x86-64 вы потеряете perf, потому что вызывающий может просто передать их в регистры.

также:

j,h,l,i=0;  // sets i=0, does nothing to j, h, or l.
       // gcc warns: left-hand operand of comma expression has no effect
j;h;l;i=0;  // equivalent to this

j=h=l=i=0;  // This is probably what you meant

Все локальные переменные все равно не используются, кроме offset. Вы собираетесь написать его на C или что-то в этом роде?

Вы используете 16-битные адреса для characters, но 32-разрядные режимы адресации для VGA-памяти. Я предполагаю, что это намеренно, но я понятия не имею, правильно ли это. Кроме того, вы уверены, что следует использовать переопределение CS: для нагрузок от characters? Включен ли раздел .rodata в сегмент кода? Хотя вы не объявили uint8_t characters[464] как const, поэтому он, вероятно, только в разделе .data в любом случае. Я считаю, что мне повезло, что я на самом деле не написал код для сегментированной модели памяти, но это все еще выглядит подозрительным.

Если вы действительно используете djgpp, то, по словам Майкла Пётча, ваш код будет работать в 32-битном режиме. Таким образом, использование 16-битных адресов - плохая идея.


Оптимизация

Вы можете полностью использовать %ebx, выполнив это, вместо загрузки в ebx, а затем добавив %ebx в %edi.

 "add    _VGA, %%edi\n\t"   // load from _VGA, add to edi.

Вам не нужно lea, чтобы получить адрес в регистре. Вы можете просто использовать

    "mov    %%ax, %%si\n\t"
    "add    $_characters, %%si\n\t"

$_characters означает адрес как постоянную константу. Мы можем сэкономить много инструкций, объединив это с предыдущим вычислением смещения в массив растровых изображений characters. Форма немедленного операнда imul позволяет получить результат в %si в первую очередь:

    "movzbw _ascii_char,%%si\n\t"
       //"sub    $32,%%ax\n\t"      // AX = ascii_char - 32
    "imul   $7, %%si, %%si\n\t"
    "add    $(_characters - 32*7), %%si\n\t"  // Do the -32 at the same time as adding the table address, after multiplying
    // SI points to characters[(ascii_char-32)*7]
    // i.e. the start of the bitmap for the current ascii character.

Так как эта форма imul поддерживает только 16 бит 16 * 16 → 32b, 2 и 3 формы операндов imul могут использоваться для подписанных или unsigned множится, поэтому только imul (not mul) имеет эти дополнительные формы. Для больших умножений размера операндов операнд 2 и 3 imul быстрее, потому что ему не нужно хранить верхнюю половину в %[er]dx.

Вы могли бы немного упростить внутренний цикл, но это немного усложняло бы внешний цикл: вы могли бы разветкить флаг нуля, как установлено shl $1, %al, вместо использования счетчика. Это сделало бы его также непредсказуемым, например, переход на хранилище для пикселей без переднего плана, поэтому увеличенные неверные предсказания отрасли могут быть хуже, чем дополнительные циклы do-nothing. Это также означало бы, что вам придется пересчитывать %edi во внешнем цикле каждый раз, потому что внутренний цикл не будет запускаться постоянным числом раз. Но это может выглядеть так:

    ... same first part of the loop as before
    // re-initialize %edi to first_pixel-1, based on outer-loop counter
    "lea  -1(%%edi), %%ebx\n"
    ".Lbit_loop:\n\t"      // map the 1bpp bitmap to 8bpp VGA memory
        "incl   %%ebx\n\t"       // inc before shift, to preserve flags
        "shl    $1,%%al\n\t"
        "jnc    .Lskip_store\n\t"   // transparency: only store on foreground pixels
        "movb   %%dl,(%%ebx)\n"  //plot the pixel
    ".Lskip_store:\n\t"
        "jnz  .Lbit_loop\n\t"    // flags still set from shl

        "addl   $320,%%edi\n\t"  // WITHOUT the -6
        "dec    %%cl\n\t"
        "jnz  .Lbyte_loop\n\t"

Обратите внимание, что биты в ваших растровых изображениях персонажа будут отображаться в байтах в памяти VGA, например {7 6 5 4 3 2 1 0}, потому что вы тестируете бит, сдвинутый левой сдвигом. Так начинается с MSB. Биты в регистре всегда "велики". Левый сдвиг умножается на два, даже на маленько-конечной машине, такой как x86. Мало-endian влияет только на порядок байтов в памяти, а не на биты в байте, а не на байты внутри регистров.


Версия вашей функции, которая может делать то, что вы намеревались.

Это то же самое, что и ссылка godbolt.

void put_char(int x,int y){
    int offset = (y<<8) + (y<<6) + x;
    __asm__ volatile (  // volatile is implicit for asm statements with no outputs, but better safe than sorry.

        "add    _VGA, %%edi\n\t" // edi points to VGA + offset.

        "movzbw _ascii_char,%%si\n\t"   // Better: use an input operand

        //"sub    $32,%%ax\n\t"      // AX = ascii_char - 32
        "imul   $7, %%si, %%si\n\t"     // can't fold the load into this because it not zero-padded
        "add    $(_characters - 32*7), %%si\n\t"  // Do the -32 at the same time as adding the table address, after multiplying
        // SI points to characters[(ascii_char-32)*7]
        // i.e. the start of the bitmap for the current ascii character.

        "mov    $7,%%cl\n"

        ".Lbyte_loop:\n\t"
            "lodsb  %%cs:(%%si)\n\t"   //load next byte of bitmap 

            "mov    $6,%%ch\n"
        ".Lbit_loop:\n\t"      // map the 1bpp bitmap to 8bpp VGA memory
            "shl    $1,%%al\n\t"
            "jnc    .Lskip_store\n\t"   // transparency: only store on foreground pixels
            "movb   %%dl,(%%edi)\n"  //plot the pixel
        ".Lskip_store:\n\t"
            "incl   %%edi\n\t"
            "dec    %%ch\n\t"
            "jnz  .Lbit_loop\n\t"

            "addl   $320-6,%%edi\n\t"
            "dec    %%cl\n\t"
            "jnz  .Lbyte_loop\n\t"


        : 
        : "D" (offset), "d" (current_color)
        : "%eax", "%ecx", "%esi", "memory"
         // omit the memory clobber if your C never touches VGA memory, and your asm never loads/stores anywhere else.
         // but that not the case here: the asm loads from memory written by C
         // without listing it as a memory operand (even a pointer in a register isn't sufficient)
         // so gcc might optimize away "dead" stores to it, or reorder the asm with loads/stores to it.    
    );
}

Я не использовал фиктивные выходные операнды, чтобы оставить распределение регистров до усмотрения компилятора, но это хорошая идея уменьшить накладные расходы на получение данных в правильных местах для встроенного asm. (дополнительные mov инструкции). Например, здесь не было необходимости принуждать компилятор помещать offset в %edi. Это мог быть любой регистр, который мы еще не используем.