Рисование символа в памяти 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'

РЕДАКТИРОВАТЬ:

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

/**************************************************************************
 *  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 я даю функцию, она всегда отображает пиксели в одном и том же месте.

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

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-битный). (например, см. доклад Мэтта Годболта на CppCon2017: "Что мой компилятор сделал для меня в последнее время? Снятие крышки с компилятора", в котором содержится отличное введение в чтение asm x86 для начинающих и просмотр выходных данных компилятора).

Знание Asm полезно, когда вы смотрите на результаты счетчика производительности, аннотирующие разборку вашего бинарного файла (perf stat ./a.out&&perf report -Mintel: см . выступление Чендлера Каррута на CppCon2015: "Настройка C++: тесты, процессоры и компиляторы! О, Боже!"). Агрессивная оптимизация компилятора означает, что рассмотрение количества циклов / пропусков кэша / остановок на строку источника гораздо менее информативно, чем на инструкцию.

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

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


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

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

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

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

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

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

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


Аппаратное программирование напрямую является аккуратным, но не очень полезным навыком

Изучение того, как создавать графику путем непосредственного изменения видеопамяти, бесполезно, кроме как для удовлетворения любопытства о том, как работали компьютеры. Вы не можете использовать эти знания ни для чего. Существуют современные графические API-интерфейсы, позволяющие нескольким программам рисовать в своих собственных областях экрана, а также позволяющие косвенное обращение (например, рисование на текстуре вместо экрана непосредственно, так что трехмерная переворачиваемая панель alt-tab может выглядеть причудливо). Есть слишком много причин, чтобы перечислять здесь, чтобы не рисовать непосредственно на видео RAM.

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

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

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

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

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


Встроенный ассм

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

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

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


Может заставить этот беспорядок работать, может быть

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

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

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

Я использовал GCC 5.3 с-m16опция, которая отличается от использования реального 16-битного компилятора. Это все еще делает все 32-битным способом (используя 32-битные адреса, 32-битныеints, и 32-битная функция указывает на стек), но сообщает ассемблеру, что процессор будет в 16-битном режиме, поэтому он будет знать, когда испускать префиксы размера операнда и размера адреса.

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


Другие вещи не так сput_char:

Вы передаете 2 вещи (ascii_charа такжеcurrent_color) в вашу функцию через глобальные переменные, а не аргументы функции. Фу, это отвратительно.VGA а также charactersявляются константами, поэтому загрузка их из глобалов выглядит не так уж плохо. Запись в asm означает, что вы должны игнорировать хорошие методы кодирования только тогда, когда это значительно повышает производительность. Поскольку вызывающая сторона, вероятно, должна была хранить эти значения в глобальных переменных, вы ничего не сохраняете по сравнению с вызывающей стороной, хранящей их в стеке как аргументы функции. А для 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удерживает только младшие 16b из умножения 16*16 -> 32b, формы операндов 2 и 3 imul можно использовать для умножения со знаком или без знака, поэтому только imul (не mul) имеет эти дополнительные формы. Для больших умножений размера операнда, 2 и 3 операнда imul быстрее, потому что он не должен хранить верхнюю половину в %[er]dx,

Вы могли бы немного упростить внутренний цикл, но это немного усложнило бы внешний цикл: вы могли бы перейти на нулевой флаг, как установлено shl $1, %alвместо использования счетчика. Это также сделало бы его непредсказуемым, например, перепрыгнуть через запас для пикселей не переднего плана, так что увеличенные ошибочные прогнозы ветвления могут быть хуже, чем дополнительные циклы бездействия. Это также означает, что вам нужно будет пересчитать %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. Little-endian влияет только на порядок следования байтов в памяти, а не битов в байте и даже не байтов внутри регистров.


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

Это то же самое, что и ссылка на Годболт.

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's 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's 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.    
    );
}

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

Другие вопросы по тегам