Поддержка USB в KolibriOS: что внутри? Часть 1: общая схема

  • Tutorial
Архитектура USB содержит несколько уровней. На самом низком уровне специально обученное железо, называемое хост-контроллером (host controller), общается с USB-устройством специальными сигналами. Сигналы кодируют биты, биты складываются в пакеты, пакеты образуют транзакции, транзакции составляют передачи (transfers).

Я рассказываю о программной поддержке USB, поэтому уровни ниже передач почти неинтересны: за них отвечает хост-контроллер. Зато важно, какой интерфейс представляет хост-контроллер софту. Сейчас распространены три интерфейса, и постепенно распространяется четвёртый:
Аббр. Название интерфейса Версия Код поддержки контроллера в KolibriOS
UHCI Universal Host Controller Interface USB 1.1 kernel/trunk/bus/usb/uhci.inc
OHCI Open Host Controller Interface USB 1.1 kernel/trunk/bus/usb/ohci.inc
EHCI Enhanced Host Controller Interface USB 2.0 kernel/trunk/bus/usb/ehci.inc
XHCI eXtensible Host Controller Interface (новый) USB 3.0 В KolibriOS ещё не поддерживается
На этом же уровне взаимодействия с контроллерами находятся файлы kernel/trunk/bus/usb/hccommon.inc, где реализованы некоторые функции, общие для всех контроллеров, и kernel/trunk/bus/usb/init.inc, который запускает всю подсистему. Впрочем, не торопитесь пока лезть в код — во-первых, я ещё не рассказала про то, чего же ожидают от него более высокие уровни, а во-вторых, после демонстрации общей схемы я вернусь к отдельным компонентам с подробностями.


Немного теории


С программной точки зрения, любое USB-устройство представляет из себя набор конечных точек (endpoints), к которым можно открыть каналы (pipes) и организовывать передачи (transfers) различных типов. Каждая конечная точка имеет свой номер, от 0 до 15. Две конечные точки могут иметь один номер, только если они обе однонаправленные, причём одна из них предназначена для передач от хоста к устройству, а другая — в обратном направлении.

В зависимости от вида обрабатываемых передач, различают четыре вида конечных точек.

  • Управляющие передачи (control transfers) — небольшие (как правило) передачи, используемые для конфигурирования, управляющих команд и запроса статуса. Единственный тип двунаправленного обмена: здесь передаётся информация и от хоста к устройству, и от устройства к хосту. Управляющая передача состоит из трёх этапов (stages), один из которых может отсутствовать:
    1. Setup stage передаёт 8 байт информации предписанного формата от хоста к устройству. Эти байты включают в себя направление и длину данных. Общий формат и стандартные запросы описаны в основной спецификации USB, и позднее я расскажу о некоторых из них.
    2. Data stage (отсутствует, если длина данных равна нулю) передаёт данные в направлении, указанном на предыдущем этапе.
    3. Status stage передаёт в обратном направлении признак успеха обработки.

    Любое USB-устройство обязано иметь управляющую конечную точку с номером 0: с её помощью инфраструктура USB узнаёт общую информацию об устройстве (в том числе «какой драйвер загружать») и выполняет начальную настройку. Практически всем (если не вообще всем) устройствам хватает одной управляющей конечной точки, которая, возможно, обрабатывает ещё и специфические для устройства запросы.
  • Передачи массивов данных (bulk transfers). Основная «рабочая лошадка», когда данных много, и важно, чтобы они дошли без искажений, но не столь важно, сколько времени на это уйдёт, — флешки, принтеры etc. Например, типичная флешка располагает тремя конечными точками — обязательной нулевой управляющей и двумя для передачи данных в двух направлениях.
  • Передачи по прерыванию (interrupt transfers). Используются, когда данных немного, но важно, чтобы они были обработаны за определённое время, — мышки, клавиатуры. Например, типичная мышка располагает двумя конечными точками — обязательной нулевой управляющей и одной типа прерывания с требованием опрашивать каждые 10 миллисекунд (можно чаще); требуемый интервал — свойство конечной точки, он может варьироваться.
  • Изохронные передачи (isochronous transfers). Единственный тип, не гарантирующий доставку данных. Используется, когда данных много, и важно, чтобы они были обработаны за определённое время, но допускаются потери данных, — мультимедиа: веб-камеры, USB-колонки.


Время на шине USB измеряется во фреймах или микрофреймах. Фреймы появились в USB1, один фрейм — одна миллисекунда. Микрофреймы появились в USB2, в одном фрейме 8 микрофреймов. Инфраструктура USB планирует выполнение передач по прерыванию и изохронных передач на конкретные (микро)фреймы так, чтобы гарантировать запрошенный интервал времени между передачами. Планирование не имеет права использовать более 90% от фрейма или более 80% от микрофрейма. Оставшееся время (минимум 10%/20%, хотя может быть и больше, если не всё время распланировано) занимают активные управляющие передачи и, по остаточному принципу, передачи массивов данных.

Уровень поддержки каналов


Файл kernel/trunk/bus/usb/pipe.inc содержит реализацию функций работы с каналами. Достаточно подробная документация есть в kernel/trunk/docs/usbapi.txt. Сейчас реализованы 4 функции. Первые две из них:
  • Функция открытия канала USBOpenPipe, в исходном коде названная usb_open_pipe. Она принимает на вход ранее открытый канал, из которого копирует характеристики устройства, включая его координату на шине USB, и характеристики нового канала, и возвращает хэндл нового канала, или нуль при ошибке. Пока неважно, что из себя представляет этот хэндл.
  • Функция закрытия канала USBClosePipe, в исходном коде названная usb_close_pipe. Её назначение и единственный параметр достаточно очевидны.

Уровень поддержки каналов также обрабатывает событие отключения устройства, закрывая все каналы, связанные с устройством. Так что явным образом закрывать канал необязательно. Внимательный читатель здесь может возмутиться: «Получается, канал может внезапно закрыться сам по себе?» Но волноваться не стоит — событие отключения устройства, помимо обработки на текущем уровне, также транслируется «наверх». Канал окончательно закроется только после того, как всем будет дан шанс обработать событие отключения устройства. Точнее, хэндл канала можно использовать как минимум вплоть до того момента, как обработчики «сверху» закончат работу. Обработчик в драйвере описан в документации как DeviceDisconnected, хотя реальное имя может быть любым — драйвер предоставляет указатель на эту функцию.

Прежде, чем вводить следующие две функции, я должна отметить следующее. У каждого канала есть своя очередь передач. В одной очереди могут находиться несколько передач одновременно, но активной может быть только одна из них — та, которая находится в голове очереди. Следующая передача начнётся только после того, как активная передача полностью успешно закончится. Если какая-то передача закончится неуспешно, очередь остановится. Зачем нужна очередь? Для эффективности: программная обработка закончившейся передачи может потребовать некоторого времени, хост-контроллер вполне может не ждать реакции обработчика, а тем временем следовать дальше. Очереди разных каналов независимы, передачи для разных каналов выполняются параллельно.
  • Функция передачи данных USBNormalTransferAsync, в исходниках названная usb_normal_transfer_async, обслуживает сразу два типа передач: передачи массивов данных и передачи по прерыванию. Для обоих типов нужно знать сами данные для передачи в виде указатель+длина, канал в виде хэндла, возвращённого USBOpenPipe, и некоторые флаги; пока что определён только один флаг, который разрешает или запрещает короткие передачи в направлении от устройства к хосту. Само направление указывать не нужно: оно однозначно определено в момент открытия канала. Суффикс «Async» указывает на то, что функция только ставит передачу в очередь, после чего немедленно возвращается; когда передача будет закончена — выполнена успешно, неуспешно или вообще отменена в связи с отключением устройства — будет вызвана callback-функция, указатель на которую также передаётся одним из аргументов USBNormalTransferAsync. Чтобы callback-функции можно было передать какую-нибудь дополнительную информацию, вместе с указателем на функцию передаётся произвольный параметр, который будет передан без изменений.
  • Функция управляющей передачи USBControlTransferAsync, в исходном коде названная usb_control_async. Интерфейс идентичен предыдущей функции с добавлением одного параметра — указателя на 8 байт для setup stage. Направление указывать по-прежнему не нужно, но уже по другой причине: оно извлекается из данных для setup stage. Строго говоря, длину данных тоже можно было бы не указывать по той же причине, но она оставлена для унификации интерфейса.

Внимательный читатель, несомненно, заметил нехватку функций. Я работаю над этим.
К уровню поддержки каналов также относится файл kernel/trunk/bus/usb/scheduler.inc; он отвечает за планирование передач, чувствительных ко времени обработки.

Уровень логического устройства


В сводке теории я уже сказала, что любое USB-устройство обязано иметь нулевую управляющую конечную точку, посредством которой инфраструктура опрашивает устройство и выполняет начальную настройку. Файл kernel/trunk/bus/usb/protocol.inc как раз этим и занимается, основываясь на уровне поддержки каналов. Результат работы этого уровня: загруженный драйвер устройства, получивший вызов функции, описанной в документации как AddDevice. Дальше всё в руках драйвера. Первый аргумент функции AddDevice — хэндл канала, открытого к нулевой конечной точке. Используя его, драйвер может открыть дополнительные каналы, нужные ему, а также проделать дополнительную настройку через нулевую конечную точку. Возвращаемое значение AddDevice — либо нуль при ошибке, либо абстрактный параметр, который USB-инфраструктура, никак не интерпретируя, — за исключением сравнения с нулём — сохраняет внутри информации об устройстве и потом передаёт функции DeviceDisconnected, про которую я уже говорила.
Для описания двух остальных параметров функции AddDevice мне понадобится ещё немного теории.
переходник 2*PS/2 -> USB
Одно физическое устройство может предоставлять несколько интерфейсов, программируемых в той или иной степени независимо. В частности, у каждого интерфейса есть свой собственный набор конечных точек. Наглядный пример — многочисленные переходники с PS/2 на USB, предоставляющие два входа, для мыши и клавиатуры; такое USB-устройство предоставляет два несвязанных интерфейса. Менее наглядный, но более распространённый пример: USB-клавиатуры со специальными кнопками часто представляются как два независимых интерфейса, один — клавиатура со стандартными кнопками, другой — дополнительные кнопки.
USB-устройство обязано поддерживать запрос различных дескрипторов, в частности, дескриптора конфигурации. В ответ на запрос дескриптора конфигурации устройство также возвращает много связанной информации, в том числе дескрипторы всех интерфейсов и всех конечных точек, ассоциированных с интерфейсом. Дескриптор конечной точки содержит всю информацию, необходимую для открытия канала к ней.
Второй аргумент функции AddDevice — указатель на данные, ассоциированные с дескриптором конфигурации, начиная с самого дескриптора конфигурации; одно из его полей — общая длина данных. Третий аргумент функции AddDevice — указатель на дескриптор интерфейса, за который отвечает драйвер.
Если устройство реализует несколько интерфейсов, то уровень логического устройства вызовет драйвер — или несколько драйверов — несколько раз. Область ответственности одного вызова AddDevice простирается от того дескриптора интерфейса, который был ей передан, до следующего дескриптора интерфейса либо до конца данных; на этом интервале расположены дескрипторы всех конечных точек этого интерфейса.

Драйверы устройств


Это самый высокий уровень в архитектуре USB. Драйверы устройств используют API уровня поддержки каналов и информацию, собранную уровнем логического устройства, для поддержки нужной функциональности, зависящей от самого устройства.
Среди драйверов выделяется драйвер хабов kernel/trunk/bus/usb/hub.inc тем, что в некоторых аспектах он близок к уровню поддержки хост-контроллера и является составной частью инфраструктуры USB. Спецификация USB выделяет часть хост-контроллера, ответственную за контроль над USB-портами, в специальную сущность — корневой хаб; корневой хаб роднит с отдельными хабами интерфейс для других уровней, но они принципиально различны с точки зрения программирования.
Интерфейс кода поддержки хабов: при подключении устройства код сообщает коду поддержки хост-контроллера о новом устройстве; при отключении устройства код передаёт информацию об этом уровню поддержки каналов; для уровня логического устройства код предоставляет функции AddDevice+DeviceDisconnected, которые в исходном тексте называются usb_hub_init и usb_hub_disconnect соответственно, а также функцию блокировки порта, к которому подключено новое устройство.
Мышки и клавиатуры поддерживаются драйвером kernel/trunk/drivers/usbhid.asm. Поддержкой флешек занимается драйвер kernel/trunk/drivers/usbstor.asm.

Интерфейс кода поддержки хост-контроллеров


Наконец, я готова объяснить весь интерфейс, предоставляемый кодом поддержки хост-контроллеров прочим уровням. При подключении нового устройства вызывается функция usb_new_device уровня логического устройства. Здесь я должна отметить следующее: usb_new_device попытается открыть канал к нулевой точке. Но функция открытия канала требует уже открытый канал, откуда она копирует характеристики устройства. Чтобы это работало, уровень хост-контроллера создаёт псевдо-канал, в структуре которого заполняет только поля с характеристиками устройства. Открытие канала к нулевой точке создаст уже полноценный канал. Когда хост-контроллер смирился с мыслью об исчезновении канала, вызывается функция usb_pipe_closed уровня поддержки каналов. Кроме того, уровень логического устройства в ходе начальной настройки меняет параметры канала; когда хост-контроллер подтверждает, что изменения приняты, вызывается одна из функций usb_after_set_address и usb_after_set_endpoint_size уровня логического устройства. Подробнее о том, зачем это нужно, я расскажу в рамках разбора уровня логического устройства.

Функции, специфичные для конкретного хост-контроллера и вызываемые из другого кода, собраны в структуру usb_hardware_func из hccommon.inc. Она включает в себя:
  • Функции для работы с аппаратной частью каналов и очередей, используемые уровнем поддержки каналов.
  • Функции для получения и изменения адреса устройства на шине, а также размера пакета канала, используемые уровнем логического устройства.
  • Функции для работы с USB-портами: блокировка, сброс, используемые уровнем логического устройства.
  • Функцию, подготавливающая параметры для usb_new_device и вызывающая usb_new_device. Она вызывается из двух мест: изнутри этого же уровня и из кода поддержки хабов.
  • Пару функций для контроллеро-неспецифичной части этого же уровня.

Все статьи серии


Часть 1: общая схема
Часть 2: основы работы с хост-контроллерами
Часть 3: код поддержки хост-контроллеров
Часть 4: уровень поддержки каналов
Часть 5: уровень логического устройства
Часть 6: драйвер хабов

P.S. Если кто ещё не в курсе: мы собираем немного денег на Kickstarter, чтобы провести свой Summer of Code. Пока что собрано 65%, и сбор средств заканчивается 31 мая (завтра). Статья: habrahabr.ru/post/180197
KolibriOS Project Team 67,79
Быстрая операционная система для бизнеса и хобби
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 57
  • 0
    Молодцы! Так держать — восхищаюсь вашему упорству!
  • +3
    Хочу заметить, что, хотя я практически не знаю ассемблер, ваш код очень классно читать — все так здорово откомментированно.

    Мне всегда было интересно, а нельзя ли, сохранив всю мощь макроассемблера как низкоуровневого языка, доработать его так, чтобы он еще больше походил на высокоуровневые языки, чтобы снизить порог вхождения при его изучении, и одновременно облегчить поддержку кода?

    Как Вы думаете, товарищи, какого синтаксического сахара, можно было бы добавить в данный язык, так, чтобы это не повлияло на производительность?
    • +3
      Макросы — великая вещь. Можно хоть в бейсик превратить. Какой еще сахар может быть в машинных инструкциях?
      • 0
        возможно тогда стоит FORTH исспользовать? Макроассемблеры не так гибки.
        • 0
          FORTH! Как много в этом звуке для сердца русского слилось! (с)
          Для более гибкой гибкости есть C, для системного программирования большего и не нужно.
      • +13
        CleverMouse, похоже, тот ещё красноглазик (в самом положительном смысле). Чувствуется стремление к логичности и рациональности — в скурпулёзно откомментированном и отформатированном коде, в практически безошибочно написанной (и отформатированной) статье, в уважении к букве ё. У меня прям прокрастинация прошла — лечебная статья, хоть я особо и не силён в низкоуровневом программировании.
        • –1
          Это, скорее всего, свойственная для девушек аккуратность (:
          • +3
            Ну, если понимать аккуратность как стремление к упорядоченности, то она свойственна большинству тех людей, которым приходится работать с большими объёмами информации. Аккуратность, упорядоченность, архитектурность в подходах — без этого никуда, если хочешь сделать что-то действительно крупное и стоящее, например, операционную систему.
          • +3
            > в скурпулёзно откомментированном и отформатированном коде

            имхо, на асме иначе не выйдет писать.
            если код не «радует глаз» — то глаза от всех этих cli; jmp $ в отместку сбегаются в кучку и желание работать пропадает напрочь.
            впрочем, лично у меня и от чужих сорцов на высокоуровневых языках возникает неприятное чувство, если эти сорцы оформлены «не в единообразном стиле»

            но все-таки глядя на асм-код хочется взглянуть автору в глаза и спросить: «автор, ну зачем все это? ведь твой код на асм-x86 становится плохо переносимым, даже переход на 64-битную архитектуру вызовет переписку практически всего. не лучше ли писать на каком-нибудь высокоуровневом языке и обрщаться к асму лишь при острой необходимости?»
            • –1
              имхо, на асме иначе не выйдет писать.
              если код не «радует глаз» — то глаза от всех этих cli; jmp $ в отместку сбегаются в кучку и желание работать пропадает напрочь.
              Асмовский код код, который радует глаз — как правило совсем не оптимальный (с точки зрения производительности) код. Для того, чтобы планировщик процессора чувствовал себя хорошо и чтобы задержки (stalls) были минимальными, зависящие друг от друга инструкции нужно разносить, получается некая гребёнка из перемешанных инструкций, относящихся к разным задачам. Параллелизм на уровне планировщика. Посмотрите на код, который генерят современные оптимизирующие компиляторы — да там же месиво, местами очень трудно понимаемое. Вы скажите «а как же out-of-order execution, register renaming, etc. ?» — да, влияние на очень умные процессоры будет на таким значительным, но есть же ещё и Atom-ы. Написать маленькую функцию на асме, которая бы соперничала по скорости с компилятором можно, а большой проект — ИМХО, никак.
              • 0
                да, вы абсолютно правы.
                но речь у нас шла не о реализации алгоритма USB, а о офоррмлении текста программы.

                • +1
                  Оформление красивое. Для полного счастья было бы неплохо указывать размер непосредственных операндов в инструкциях push imm (например ehci.inc:333) push 32

                  И ещё интересно зачем (там же) используются inc eax; inc eax; и push 32; pop ecx;?
                  • +3
                    Вместо add eax,2 и mov ecx,32 соответственно? Так короче. inc eax однобайтовая, add eax,2 занимает 3 байта. mov с непосредственным операндом — одна из немногих команд, не имеющих опкода, где непосредственное значение хранится как байт и расширяется до требуемого размера в ходе выполнения, поэтому mov ecx,32 занимает 5 байт — один на опкод, 4 на значение 32 — в отличие от push 32 с двумя байтами — один на опкод, 1 на значение 32.
                    Это также медленнее на один-два такта, но конкретно этот участок выполняется один раз в жизни контроллера при инициализации, и несколько тактов роли не играют, а вот несколько байт заметны везде.
                    • –2
                      Ну сколько вы выиграете на всём ядре, килобайт? может быть два? А производительность приносите в жертву. Этот кусочек не единственный, вот в планировщике, подряд
                              push    5
                              pop     ecx
                              push    1
                              pop     ebx
                              push    sizeof.ehci_static_ep
                              pop     edx
                      
                      а конструкции вида
                              push imm
                              pop reg32
                              call proc
                      
                      вообще сериализация. Ну и не понятно тогда, почему же, если вы оптимизируете под размер, то всё равно много где используется нормальная загрузка значения, как
                              mov     ecx, 4
                      
                      Где же однообразие кода и подхода?

                      Да вы и сами, наверняка, знаете, что не всё с кодом оптимально, но из-за ассемблера, изменения становятся вся более трудоёмкими, поэтому начинает преобладать принцип: «работает? — не лезь!»
                      А Си-шый код так легко перекомпилировать, и новым компилятором, и под новый процессор…
                      Сейчас использую ассемблер только для PIC-микроконтроллеров — вот там он востребован, так как счёт действительно идёт на байты.
                      Вы молодцы, ваш проект — замечательный пример программирования на ассемблере, школьникам и студентам для обучения вообще бомба. Но для реальных задач, ИМХО, Сизифов труд.
                      • +2
                        А производительность приносите в жертву.

                        Это не так. Производительность не меняется. Даже если вы можете считать отдельные такты, вы собьётесь со счёта в момент сброса контроллера — он занимает больше, чем вся остальная инициализация, вместе взятая, — а 100-миллисекундный интервал, требуемый перед началом какой бы то ни было работы с USB-устройством, покажется вам вечностью.
                        Ну и не понятно тогда, почему же, если вы оптимизируете под размер, то всё равно много где используется нормальная загрузка значения, как

                        Я надеюсь, это не будет для вас шоком, но я должна признаться: я написала далеко не весь код ядра.
                        Кроме того, в некоторых местах производительность таки важнее размера.
                        А Си-шый код так легко перекомпилировать, и новым компилятором, и под новый процессор…

                        … и какая разница, что размер результата будет в разы больше…
                        Да вы и сами, наверняка, знаете, что не всё с кодом оптимально, но из-за ассемблера, изменения становятся вся более трудоёмкими, поэтому начинает преобладать принцип: «работает? — не лезь!»

                        Вы серьёзно хотите продемонстрировать тезис о трудоёмкости изменений задачей, решаемой заменой по регэкспу? Специально для вас в свежей ревизии ядра я заменила пары push / pop на специальный макрос, который можно определить как простой mov, а можно определить так, как он сейчас определён — парой push / pop. Для критичных ко скорости участков по-прежнему есть mov.
                        • 0
                          По производительности — второй пример был из планировщика ehci_select_hs_interrupt_list — там же не вызывается сброс контроллера каждый раз?

                          Трудоёмкость — да, очень хочу убедить, так как шишек много. Опять таки, это был первый попавшийся на глаза пример. Если очень интересно — можно ещё найти места, где используются лишние копирования (потому что человеку не под силу держать в голове текущий контекст программы), где инструкции идут не в самом благоприятном порядке — это всё макросами не исправишь (movi — оперативненько!)

                          И насчёт «размер результата будет в разы больше» это вы погорячились. Я тоже так думал, ровно до тех пор, пока компилятор не стал генерить сравнимый (а зачастую и более короткий и быстрый код). Учитывая разницу во времени на получение этого результата, я крепко призадумался. Потом переписывал только маленькие критические кусочки, потом интринсики.
                          • +2
                            По производительности — второй пример был из планировщика ehci_select_hs_interrupt_list — там же не вызывается сброс контроллера каждый раз?

                            Нет, планировщик вызывается при открытии канала, то есть несколько раз при начальной конфигурации устройства — требующей как минимум тех самых 100 мс в начале. На фоне которых говорить о паре тактов просто смешно.

                            Трудоёмкость — да, очень хочу убедить

                            Речь шла про трудоёмкость изменения кода только из-за того, что он на ассемблере. Я думаю, что демонстрация была достаточно убедительной, чтобы этот миф можно было закрыть.

                            так как шишек много. Опять таки, это был первый попавшийся на глаза пример.

                            Это неубедительное теоретизирование. Убедительной демонстрацией было бы «вот, смотрите, я заменил movi на mov и теперь <что-нибудь> занимает не две секунды, а одну».

                            Если очень интересно — можно ещё найти места, где используются лишние копирования (потому что
                            человеку не под силу держать в голове текущий контекст программы)

                            Вот это интересно, подобное ещё и размер раздувает. Найдите — я исправлю и скажу «спасибо».

                            где инструкции идут не в самом благоприятном порядке — это всё макросами не исправишь

                            Инструкции можно переставить. Но здесь уже надо доказывать, что перестановка инструкций что-то даст.

                            (movi — оперативненько!)

                            Спасибо. Я стараюсь.

                            И насчёт «размер результата будет в разы больше» это вы погорячились. Я тоже так думал, ровно до тех пор, пока компилятор не стал генерить сравнимый (а зачастую и более короткий и быстрый код).

                            Распространённое заблуждение. Я приведу два примера.

                            Пример 1. У нас есть драйверы для видеокарт Intel и ATI, портированные с Linux, — естественно, на Си. Их нет в дистрибутиве: в образ они не влезают, а методы, позволяющие обратиться к дополнительным источникам данных, где бы они ни были, пока в разработке — но их без проблем можно найти на форуме. Так вот, дословный кусок кода из одной версии одного из драйверов:
                            .text:000033EA                 movzx   edx, byte ptr [eax+ebx+3]
                            .text:000033EF                 shl     edx, 8
                            .text:000033F2                 movzx   esi, byte ptr [eax+ebx+2]
                            .text:000033F7                 or      esi, edx
                            .text:000033F9                 shl     esi, 10h
                            .text:000033FC                 movzx   edx, byte ptr [eax+ebx+1]
                            .text:00003401                 shl     edx, 8
                            .text:00003404                 movzx   eax, byte ptr [eax+ebx]
                            .text:00003408                 or      eax, edx
                            .text:0000340A                 movzx   eax, ax
                            .text:0000340D                 or      esi, eax
                            

                            Упражнение на понимание: выяснить, что делает код, записать результат одной ассемблерной командой и сравнить размеры.

                            Пример 2. Типичный фрагмент программы на Си:
                            extern void f(void* something, int x, int y, int z);
                            ...
                            void* p;
                            ...
                            f(p, 1, 2, 3);
                            

                            Вменяемый ассемблерщик в зависимости от желания/наличия макросов может написать либо
                            ccall f,[p],1,2,3
                            

                            либо, что то же самое,
                            push 3
                            push 2
                            push 1
                            push [p]
                            call f
                            add esp,10h
                            

                            Если p — локальная переменная и создан кадр стека, то [p] — что-то типа [ebp-4] и конструкция занимает 2*3+3+5+4 = 18 байт.
                            Барабанная дробь, gcc с ключом -Os, который якобы означает «оптимизировать по размеру»:
                            mov eax,[p]
                            mov dword[esp+12],3
                            mov dword[esp+8],2
                            mov dword[esp+4],1
                            mov dword[esp],eax
                            call f
                            

                            Здесь кадра стека уже не будет, и [p] — что-то типа [esp+20]. Теперь конструкция занимает 4+8*3+3+5 = 36 байт. Разница, как видно, в два раза. Сравнимый код, говорите?
                            • 0
                              Пример 2. Типичный фрагмент программы на Си:...gcc с ключом -Os, который якобы означает «оптимизировать по размеру»

                              … у меня генерирует следующее:
                              $ cat | gcc -Os -xc - -S -m32 -g0 -o -
                              extern void f(void* something, int x, int y, int z);
                              void* p;
                              void g(void)
                              {
                                      f(p, 1, 2, 3);
                              }
                                      .file   ""
                                      .text
                                      .globl  g
                                      .type   g, @function
                              g:
                              .LFB0:
                                      .cfi_startproc
                                      pushl   %ebp
                                      .cfi_def_cfa_offset 8
                                      .cfi_offset 5, -8
                                      movl    %esp, %ebp
                                      .cfi_def_cfa_register 5
                                      subl    $8, %esp
                                      pushl   $3
                                      .cfi_escape 0x2e,0x4
                                      pushl   $2
                                      .cfi_escape 0x2e,0x8
                                      pushl   $1
                                      .cfi_escape 0x2e,0xc
                                      pushl   p
                                      .cfi_escape 0x2e,0x10
                                      call    f
                                      addl    $16, %esp
                                      .cfi_escape 0x2e,0
                                      leave
                                      .cfi_restore 5
                                      .cfi_def_cfa 4, 4
                                      ret
                                      .cfi_endproc
                              .LFE0:
                                      .size   g, .-g
                                      .comm   p,4,4
                                      .ident  "GCC: (GNU) 4.6.3 20120306 (Red Hat 4.6.3-2)"
                                      .section        .note.GNU-stack,"",@progbits
                              

                              что в точности соответствует… Может вы его готовить не умеете?
                              • +3
                                $ cat | gcc -Os -xc - -S -m32 -g0 -o -
                                extern void f(void* something, int x, int y, int z);
                                void* p;
                                void g(void)
                                {
                                        f(p, 1, 2, 3);
                                }
                                        .file   ""
                                        .text
                                .globl _g
                                        .def    _g;     .scl    2;      .type   32;     .endef
                                _g:
                                        pushl   %ebp
                                        movl    %esp, %ebp
                                        subl    $24, %esp
                                        movl    _p, %eax
                                        movl    $3, 12(%esp)
                                        movl    $2, 8(%esp)
                                        movl    $1, 4(%esp)
                                        movl    %eax, (%esp)
                                        call    _f
                                        leave
                                        ret
                                        .comm   _p, 4, 2
                                        .def    _f;     .scl    2;      .type   32;     .endef
                                $ gcc --version
                                gcc (GCC) 4.5.3
                                Copyright (C) 2010 Free Software Foundation, Inc.
                                Это свободно распространяемое программное обеспечение. Условия копирования
                                приведены в исходных текстах. Без гарантии каких-либо качеств, включая
                                коммерческую ценность и применимость для каких-либо целей.
                                
                                • 0
                                  Что ж, по крайней мере сгенерированный ассемблерный код улучшается со временем без нашего вмешательства, в отличие от однажды написанного ассемблерного кода.
                                  • +1
                                    Увы, нет. Я сравнивала при настройке автосборки актуальные на тот момент версии веток gcc3 и gcc4, и gcc4 сливал с треском, передача параметров mov'ом при -Os — лишь наиболее наглядный пример. Я рада, что в gcc 4.6 конкретно эту деталь наконец-то починили, но это отнюдь не единственная деталь.

                                    Кроме того, почитайте обсуждение — все фокусируются на быстродействии, а не на размере. В реальных проектах компиляция идёт не с -Os, а с -O2 — ведь большой размер — это даже престижно.
                                    • 0
                                      Кроме того, почитайте обсуждение — все фокусируются на быстродействии, а не на размере. В реальных проектах компиляция идёт не с -Os, а с -O2 — ведь большой размер — это даже престижно.
                                      А у вас размер — это самоцель какая-то? На PC расширить память — не проблема, увеличить производительность — да. Для маленьких/встраиваемых устройств, там где размер имеет значение, компилируется с -Os (например в XCode под iOS Release).

                                      По производительности — скажите, где у вас профайлер показывает затыки, и я попробую пооптимизировать. Но опять таки, под какой процессор, какую память?
                                      • +1
                                        У нас нет ни профайлера, ни затыков :-) Мы даже никого не заставляем использовать KolibriOS. Просто рассказываем о себе, чтобы те, кому мы понравились или нужны, узнал о том, что мы есть, и как нас найти.
                                      • 0
                                        Увы, нет.

                                        — т.е. ваш ассемблерный код таки становится лучше без вашего вмешательства? Поделитесь секретом?

                                        все фокусируются на быстродействии, а не на размере

                                        Мне показалось, что обсуждение фокусируется на гибкости: пропоненты компиляции говорят, что при разработке на С/… у вас есть выбор: чем компилировать, как компилировать, подо что компилировать.
                                        • +1
                                          Нет, сишный код раздувается просто от того, что его перекомпилировали gcc4 вместо gcc3.
                                          Мне показалось, что обсуждение фокусируется на гибкости

                                          Я отвечу не своей цитатой выше по ветке:
                                          Ну сколько вы выиграете на всём ядре, килобайт? может быть два? А производительность приносите в жертву


                                          у вас есть выбор: чем компилировать, как компилировать, подо что компилировать.

                                          Если результат при любом из выборов получается хуже, чем при написании на ассемблере, то факт наличия выбора особого значения не имеет.
                                    • +1
                                      Кстати, вы не думали о том, чтобы работать над улучшением кода генерируемого gcc, вместо работы над конкретным ассемблерным кодом? На мой взгляд, это по всем параметрам более благородная задача.
                                      • 0
                                        Из-за сложности модификации кодогегерации в gcc появился llvm и clang именно туда большинство усилий оптимизаторов устремлено. Код gcc очень сложно расширяемый и с каждым годом теряет мейнтенеров.
                                        • 0
                                          Ок, «gcc» в моём комментарии можно заменить на «ваш любимый/ваш целевой компилятор С».
                                        • 0
                                          А Вы не думали о том, чтобы делать лучше свою операционную систему, вместо того, чтобы хаять нашу? Глядишь, наши разработчики так впечатлятся, что сами перейдут к Вам.
                                          • +3
                                            вместо того, чтобы хаять нашу

                                            Интересный поворот. Не имел намерений. Даже странно, какая моя фраза вас так задела.
                                            По части фантома — я позанимался тем, что мне было в нём интересно. Пока продолжать большого смысла нет.
                                            С другой моей операционной системой всё в порядке, скоро будет SMP, можно переходить (:
                                            • 0
                                              Ну, хотя бы совет, данный CleverMouse, «работать над улучшением кода генерируемого gcc, вместо работы над конкретным ассемблерным кодом». Мы же Вам не говорим, чем Вам заниматься, и не считаем Ваши занятия недостаточно благородными. За пример с GCC 4.6.3 Вам спасибо, кстати.
                                              • 0
                                                Ну вообще-то это был вопрос, и мне действительно интересно, что об этом думает CleverMouse.
                                                • +2
                                                  Я в некоторый момент думала над улучшением одного момента в кодогенерации gcc. Потом я заглянула в исходники, закрыла их и бросила все мысли в этом направлении.
                                    • 0
                                      Упражнение на понимание:
                                      Как я писал выше, в асме очень важен контекст. В данном случае у меня есть предположение, что где-то выше, эти данные были записаны как байты, а значит читать их двойным словом будет медленно. Не нравятся кэшам такие «оптимизации».

                                      С компилятором GCC: (Gentoo 4.7.3 p1.0, pie-0.5.5) 4.7.3" код такой же, что и у jcmvbkbc.

                                      И ещё интересная инфа, родной маковский nasm (NASM version 0.98.40 (Apple Computer, Inc. build 11) compiled on Feb 6 2013) кодирует push imm8 (без префикса размера) в 5 байт, также как если указать DWORD, а вот с BYTE — в два байта.
                                      • +2
                                        Если всё же нужно прочитать двойное слово и аппаратура умеет это делать, то программная эмуляция действий аппаратуры будет заведомо медленнее самой аппаратуры. Но я привела этот код в качестве примера к тезису «код раздувается в разы».

                                        Ядро KolibriOS написано на fasm, а не на nasm, и с точки зрения fasm инструкция «push byte 1» недопустима, потому что не существует опкода, кладущего в стек именно байт.
                                        • 0
                                          Если всё же нужно прочитать двойное слово и аппаратура умеет это делать, то программная эмуляция действий аппаратуры будет заведомо медленнее самой аппаратуры.
                                          В таком случае не было многотомных Optimization Guide-ов, лекций и семинаров посвящённых данному вопросу, так как следую вашей логике — раз аппаратура умеет сделать что-то одной инструкцией, то нечего пытаться заменить это несколькими?
                                          • +3
                                            Ну что же, контекст: это разбор таблиц AtomBIOS, в которые ни ядро, ни драйвер не пишут ни байтами, ни как бы то ни было ещё. Теперь сможете привести конкретную цитату из «многотомных Optimization Guide-ов», которая бы оправдывала такой код?
                                            • 0
                                              BIOS на видеокартах — это обычно Serial EEPROM, который отображается в адресное пространство процессора страницами со специальными атрибутами. Это при условии, что пользователь не установил в своих настройках BIOS-а материнской платы галочку "Video ROM BIOS Shadow", тогда всё шоколадно.
                                              Доступ к EEPROM памяти, находящейся на другой плате, через несколько последовательных шин — довольно небыстрая процедура, ширина — 8 бит (да, да — те самые байты), читать надо подряд, иначе процессор обидится, и придётся чуток подождать.

                                              Таким образом, вы должны гордится автором того кусочка, так как он сделал минимально возможный размер кода, исключив функцию определения типа страницы памяти и не дублируя приведённый кусочек кода (он же там больше) под разные сценарии.
                              • 0
                                Я не совсем понял объяснение — «mov ecx,32» и байт больше занимает, и выполняется медленнее на 1-2 такта, чем «push 32; pop ecx;», или байт занимает больше, но выполняется быстрее?
                                • +2
                                  Оно длиннее, но быстрее. Альтернативный вариант короче, но медленнее.
                    • +5
                      Если добавить к ассемблеру достаточное для более-менее комфортного использования количество сахара, получится ANSI C 89 :)
                      При правильном применении производительность почти та же.
                      Ну, или урезаные версии, вроде C--
                      • 0
                        Хм… а как вы думаете — ребята, которые Колибри пишут, просто не в курсе, что «производительность почти та же», или у них другое мнение?

                        Может, им стоило бы как раз на Анси-С или С-- все писать?.. И гибко, и проще, вроде бы…
                        • +1
                          просто не в курсе, что «производительность почти та же», или у них другое мнение

                          Я думаю, среди них есть разные люди. Для кого-то это религия, для кого-то круто писать ОС на асме, кто-то хочет чтобы в проект не лезли не осилившие.
                          С точки зрения производительности 90% кода вообще всё равно на чём писать, если дизайн правильный.
                        • +1
                          Ну, я не в курсе насчёт именно Колибри, но писал под MenuetOS и там — да, все в курсе, но идея именно писать на ассемблере.
                          Тотальный контроль аппаратной части :)
                      • 0
                        Мне всегда было интересно, а нельзя ли, сохранив всю мощь макроассемблера как низкоуровневого языка, доработать его так, чтобы он еще больше походил на высокоуровневые языки, чтобы снизить порог вхождения при его изучении, и одновременно облегчить поддержку кода?

                        Вы сейчас говорите про С--
                        Периодически всплывает, иногда на нём даже что-то пишут.
                        Но win-а не получается. Люди обычно в обе стороны от него уходят. Кто-то в сторону чистого С (и выше), кто-то в сторону чистого ассемблера.
                        • 0
                          Почитал про него. Пожалуй, это примерно то, что я имел в виду. Правда, пишут, что он больше для промежуточной компиляции, нежели для непосредственного программирования…
                    • 0
                      Для любителей извратаысканных удовольствий — ООП на ассемблере
                      • +1
                        Вопрос к CleverMouse
                        1) по какой причине стандартные драйвера для HID были включены в состав драйвера?
                        2) не проще ли сделать внешний интерфейс для драйвера, а драйвера реализовать как службы?
                        3) возможно было бы проще реализовать HID драйвера на С-- в качестве примера разделения логики контроллера USB от драйверов профилей?
                        • +4
                          1) Выделение части, работающей с HID, в отдельный драйвер имело бы смысл, если было бы несколько различных источников данных HID. Пока источник один и других не просматривается, это лишь ненужное усложнение.
                          2) Этот вопрос я не совсем поняла. Если «службой» вы называете отдельное usermode-приложение — фактически тот же драйвер, но в usermode — то писать сам драйвер проще бы не было — действия ровно те же самые. Возможно, было бы проще его отлаживать, несомненно, были бы куда легче последствия от того, что что-нибудь пошло не так. Ядру было бы, наоборот, сложнее. Я думаю над чем-нибудь типа libusb, но это явно неприоритетная вещь. Если вы имеете в виду что-то другое — уточните.
                          3) Ещё проще было бы ничего не делать. Естественно, это не позволило бы достичь цели. Если бы я ставила цель продемонстрировать какой-нибудь пример — скорее всего, я бы использовала Си как язык, понятный достаточно многим программистам и не слишком мешающий низкоуровневым вещам. Если бы я ставила цель реализовать пример какого бы то ни было USB-драйвера — я рассмотрела бы также вариант с той же libusb и, например, python, это бы ещё расширило круг читателей. Но моя цель — написать операционную систему возможно меньшего размера в той степени, пока это не мешает производительности, и C--, C, C++ не годятся для этого совсем никак.
                          • +1
                            1) Вопрос тогда закрыт
                            2) Вы все верно поняли — примерно таким же образом реализованы функции в QNX
                            3) Поскольку ответ на 1 вопрос закрыт — то 3 тоже не имеет смысла, раз разделения не требуется.
                        • 0
                          А у меня тоже вопрос к разработчикам: на сколько, по вашим личным ощущениям, более трудоёмко писать на асме, чем на том же c/c--?
                          • +3
                            Зависит и от разработчика, и от задачи.
                            В основном (на работе) я пишу на C и python, в Колибри преимущественно занимаюсь кодом тоже на Си. Когда я разрабатывал драйверы для принтера, я сначала написал прототип на python+libusb, а затем реализовал примерно то же на ассемблере (при помощи опытных людей, в частности, CleverMouse) — и был приятно удивлен тем, что задача, в общем-то, оказалась решена чуть ли не проще, чем на python.
                          • –5
                            Clever Mouse? Гаечка, так ты всё таки существуешь? Потому что написать реализацию USB на Ассемблере, по-моему, способна только одна умная мышь

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое