Критические замечания к статьям из "Вестника КолибриОС", выпуск 1. ============================================================================== === Прикладное программирование для Kolibri OS. Вводный курс. Часть 1 ======== ============================================================================== 0. Структура заголовка на примере ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ db 'MENUET01' ; 1. идентификатор исполняемого файла (8 байт) dd 0x01 ; 2. версия формата заголовка исполняемого файла dd START ; 3. адрес, на который система передаёт управление ; после загрузки приложения в память dd I_END ; 4. размер приложения ; Замечание 1. Программисты-ядерщики сами ещё не определились, что же ; должно быть в этом поле и как его использовать. Пока что следует ; здесь указывать размер исполняемого файла, он же размер всех ; инициализированных данных и кода. Метка I_END находится в программе ниже, ; на границе инициализированных и неинициализированных данных. ; Комментарий. Смещение метки в Колибри-программах совпадает с размером ; кода и данных до этой метки, поскольку программа начинается с адреса 0. dd 0x100000 ; 5. объём необходимой приложению памяти ; можно обращаться к памяти в диапазоне от 0x0 ; до значения, определённого здесь ; Замечание 2. В нашем примере указано 0x100000 байт = 1 Мб. Этого заведомо ; достаточно. Вероятно, методы более точного определения объёма используемой ; памяти будут приведены в следующих статьях цикла. ; Исторический комментарий. Традиция требовать 1 Мб памяти даже для самых ; маленьких программ идёт от не уважаемого в среде Колибри-программистов ; Велика (Ville Turjanmaa), создателя и могильщика MenuetOS - предшественницы ; KolibriOS. Массовое уменьшение объёма требуемой памяти в существующих ; приложениях провёл очень уважаемый в той же среде Mario79. dd 0x100000 ; 6. вершина стека в диапазоне памяти, указанном выше dd 0x0 ; 7. указатель на строку с параметрами. ; если после запуска неравно нулю, приложение было ; запущено с параметрами из командной строки ; Замечание 3. Предыдущий комментарий неверен. Этот пример не обрабатывает ; параметры командной строки (в самом деле, зачем они ему?), в таких случаях ; здесь должен быть указан 0 (чтобы система знала, что командную строку ; передавать не надо). Пример, обрабатывающий командную строку, ; вероятно, будет в следующих статьях цикла. dd 0x0 ; 8. указатель на строку, в которую записан путь, ; откуда запущено приложение ; Замечание 4. См. замечание 3 с заменой "командной строки" на "путь к ; исполняемому файлу". 1. Основной цикл ~~~~~~~~~~~~~~~~ На событие 2 (нажатие клавиши) можно в принципе не реагировать, если вам это не требуется. Замечание 5. Можно. Но не рекомендуется, хотя и не по той причине, что указана в тексте. Дело в том, что сообщения о нажатии клавиш приходят, пока вы не прочитаете все клавиши из буфера. Соответственно, если вы не будете реагировать, то это сообщение будет приходить снова и снова. В качестве эксперимента можете закомментировать, например, вызов функции 2 в коде, приводимом ниже - нагрузка на процессор подскочит до 100%, а события о нажатии кнопок вообще не будут приходить, поскольку их приоритет (по крайней мере, в текущей реализации ядра) ниже приоритета события о нажатии клавиши. Основной цикл выглядит так: START: ; адрес начала программы call draw_window ; вызываем функцию рисования окна ; затем переходим в цикл ожидания событий event_wait: mov eax,10 ; функция 10: ожидание события int 0x40 ; тип события возвращён в eax, далее проверяем, какое событие произошло cmp eax,1 ; запрос на перерисовку? je redraw cmp eax,2 ; нажата клавиша? je key cmp eax,3 ; нажата кнопка в окне программы? je button jmp event_wait ; возвращаемся к началу цикла ожидания событий ; после того, как событие идентифицировано, его надо обработать redraw: ; пришёл запрос на перерисовку! call draw_window ; вызываем функцию draw_window и jmp event_wait ; возвращаемся назад к циклу ожидания key: ; была нажата клавиша! mov eax,2 ; считываем код нажатой клавиши. Возвращен в ah. int 0x40 ; Клавиша должна быть прочитана для очистки ; системного буфера. jmp event_wait ; возврат к event_wait button: ; была нажата кнопка в окне! mov eax,17 ; считываем идентификатор нажатой кнопки int 0x40 ; возвращен в ah. ; смотрим, какая кнопка была нажата и соответствующим образом реагируем. cmp ah,1 ; кнопка с id=1("закрыть")? jne noclose mov eax,-1 ; функция -1: завершить программу int 0x40 noclose: ; здесь проверяем остальные кнопки (если они есть) jmp event_wait ; и, конечно, возвращаемся к циклу ожидания :) Исторический комментарий. Такой способ написания главного цикла идёт всё от того же Велика. И у этого способа есть несколько недостатков. Замечание 6. Обратите внимание на код, выполняемый по метке redraw. А теперь переведите взгляд на метку START. Как говорится, "найдите 10 отличий"... Отличий нет! Так что можно спокойно удалить код, находящийся на метке redraw (до метки key), а саму метку redraw перенести непосредственно перед строкой с вызовом draw_window. Замечание 7. - Зачем вам два jmp'а подряд? - А вдруг первый не сработает... Функция 10 всегда возвращает в eax код одного из отслеживаемых событий. (Вы не верите системе? Поверьте на слово, уж если система захочет подшутить над вашей программой, у неё найдётся много гора-а-аздо более интересных способов это сделать. Следуя подобной логике, можно перестать доверять и процессору тоже.) Так что если в eax не 1 и не 2, то там точно 3 и можно это даже не проверять. Впрочем, в данном случае стоит проверить eax на равенство 1 (с переходом на метку redraw, которую уже перенесли вверх), на равенство 3 (с переходом вниз на button), после чего останется только вариант eax=2, так что можно перенести метку key с кодом на место сразу после сравнений, сэкономив тем самым jmp на эту метку. (Хотите код? Немного потерпите, у меня ещё замечания не кончились.) Замечание 8. Приведённый код, безусловно, работает. Тем не менее любители (и профессионалы) Ассемблера обычно не любят сильно неоптимальный код (а иначе зачем вообще Ассемблер в наше время быстрых процессоров, больших жёстких дисков и огромным количеством языков высокого уровня? На некоторых из которых можно спокойно программировать в Колибри, но это тема отдельного разговора). Маленькое замечание: инструкция dec eax занимает один байт, инструкция cmp al,1 занимает 2 байта, инструкция cmp eax,1 занимает 3 байта. Если оптимизировать по размеру кода, лучше писать так: START: ; адрес начала программы ; ... здесь может быть код инициализации ... redraw: call draw_window ; вызываем функцию рисования окна ; затем переходим в цикл ожидания событий event_wait: mov eax,10 ; функция 10: ожидание события int 0x40 ; тип события возвращён в eax, далее проверяем, какое событие произошло dec eax ; eax=1? запрос на перерисовку? je redraw dec eax jne button ; если мы здесь, то функция 10 вернула eax=2 - нажата клавиша key: ; была нажата клавиша! ; Заметьте, что в этот момент eax=0, поскольку мы можем попасть сюда ; только прямым путём сверху вниз, а на этом пути eax обнулено ; Так что результаты выполнения mov al,2 и mov eax,2 одинаковы, скорость : выполнения одинакова, но первый вариант на 3 байта короче! mov al,2 ; считываем код нажатой клавиши. Возвращен в ah. int 0x40 ; Клавиша должна быть прочитана для очистки ; системного буфера. jmp event_wait ; возврат к event_wait ; если мы здесь, то функция 10 вернула не 1 и не 2 - это может быть только 3 button: ; была нажата кнопка в окне! ; В этот момент eax=1, поскольку мы можем попасть сюда только переходом ; jne несколькими строчками выше, а на этом пути eax=3-2=1 ; Следовательно, применимо замечание, аналогичное замечанию на метке key mov al,17 ; считываем идентификатор нажатой кнопки int 0x40 ; возвращен в ah. ; смотрим, какая кнопка была нажата и соответствующим образом реагируем. cmp ah,1 ; кнопка с id=1("закрыть")? jne noclose or eax,-1 ; функция -1: завершить программу int 0x40 noclose: ; здесь проверяем остальные кнопки (если они есть) jmp event_wait ; и, конечно, возвращаемся к циклу ожидания :) Замечание 9. Помимо оптимизации по размеру кода бывает оптимизация по скорости выполнения. В данном случае она, правда, не имеет особого смысла, поскольку вызываются несколько системных функций (запомните раз и навсегда: вызов любой системной функции - медленная вещь). На многих процессорах инструкции inc/dec выполняются не очень быстро (медленнее своих аналогов add/sub xxx,1). Комментарий для специалистов: ...поскольку не меняют флаг CF, что порождает ложные зависимости от предыдущих команд. Если оптимизировать по скорости, лучше писать так: START: ; адрес начала программы ; ... здесь может быть код инициализации ... redraw: call draw_window ; вызываем функцию рисования окна ; затем переходим в цикл ожидания событий event_wait: mov eax,10 ; функция 10: ожидание события int 0x40 ; тип события возвращён в eax, далее проверяем, какое событие произошло cmp al, 1 je redraw ; запрос на перерисовку? cmp al, 2 jne button ; если мы здесь, то функция 10 вернула eax=2 - нажата клавиша key: ; была нажата клавиша! ; Заметьте, что в этот момент eax=2, поскольку мы можем попасть сюда ; только прямым путём сверху вниз ; mov eax,2 ; это уже так int 0x40 ; Клавиша должна быть прочитана для очистки ; системного буфера. jmp event_wait ; возврат к event_wait ; если мы здесь, то функция 10 вернула не 1 и не 2 - это может быть только 3 button: ; была нажата кнопка в окне! ; В этот момент eax=3, поскольку мы можем попасть сюда только переходом ; jne несколькими строчками выше, а на этом пути eax=3 ; Следовательно, можно писать mov al,17 вместо mov eax,17 mov al,17 ; считываем идентификатор нажатой кнопки int 0x40 ; возвращен в ah. ; смотрим, какая кнопка была нажата и соответствующим образом реагируем. cmp ah,1 ; кнопка с id=1("закрыть")? jne noclose or eax,-1 ; функция -1: завершить программу int 0x40 noclose: ; здесь проверяем остальные кнопки (если они есть) jmp event_wait ; и, конечно, возвращаемся к циклу ожидания :) ============================================================================== === Использование компонента checkbox ======================================== ============================================================================== dd i_end ; размер образа dd i_end+0x100 ; Объем используемой памяти, для стека отведем 0х100 байт dd i_end+0x100 ; расположим позицию стека в области памяти Замечание 1. Это совершенно верно, но следует отметить, что данная программа не использует неинициализированных данных, так что i_end (конец инициализированных данных) одновременно означает конец используемой памяти в целом, так что стек, размещённый в 0x100 байтах после i_end не затрёт никаких наших данных. Так что будьте осторожны при переносе этого на свои собственные программы! Замечание 2. Для повышения скорости команд работы со стеком рекомендуется, чтобы стековый указатель (регистр esp) был выровнен на 4 байта. Так что вместо последних строк лучше писать так: dd (i_end+0x100) and not 3 dd (i_end+0x100) and not 3 xor ebx,ebx ; обнулить регистр ebx add ebx,2 ; добавить в регистр ebx значение =2 Замечание 3. Это пример неоптимального во всех смыслах кода. Этот код занимает 5 байт и выполняется 2 такта (каждая инструкция занимает 1 такт и инструкции неспариваемы, поскольку вторая зависит от первой). По скорости лучше всего использовать команду mov (на то она и придумана): mov ebx, 2 По размеру кода лучше всего использовать конструкцию push 2 pop ebx - это 3 байта и те же 2 такта. Что предпочесть - выбирайте сами. Советую оптимизацию по размеру кода, поскольку разница в скорости просто незаметна на фоне очень "тяжёлых" инструкций "int 0x40". Такие же ляпы есть и ниже по тексту, в дальнейшем я на них останавливаться не буду. cmp eax,0x1 ; если изменилось положение окна jz red_win cmp eax,0x2 ; если нажата клавиша то перейти jz key cmp eax,0x3 ; если нажата кнопка то перейти jz button mouse_check_boxes check_boxes,check_boxes_end ; проверка чек бокса jmp still ; если ничего из перечисленного то снова в цикл Замечание 4. В отличие от предыдущей статьи, здесь используется функция 23, которая может возвращать значение 0 в случае таймаута. Так что здесь уже законно сравнение со всеми значениями 1,2,3. Замечание 5. Можно выиграть в размере кода без потери скорости, если сравнивать al, а не eax. Замечание 6. Приложение "просыпается" каждые 2/100 секунды для проверки того, что произошло с мышью. При этом в большинстве случаев просто зря тратится процессорное время. Оптимальной реализацией является установка в маске событий реакции на мышь (событие 6) (функцией 40), ожидание в 10-й функции, проверка мыши в ответ на событие 6. xor eax,eax ; начало рисования обнулить регистр eax add eax,12 ; добавить в регистр значение eax 12 xor ebx,ebx ; обнулить регистр ebx add ebx,1 ; быстрее на скалярных процессорах прибавить 1 xor eax,eax ; обнулить eax Замечание 7. Явный ляп - забыта инструкция "int 0x40" после первых 4-х строк. mov bx,ch_left ; положение по х mov cx,bx ; сохраним в регистре cx значение bx Замечание 8. Для checkbox.inc вообще очень характерно использование 16-битной арифметики. В 32-битном режиме это чревато лишним байтом и лишним тактом. mov ecx,ebx было бы лучше. А также желательно сделать все данные 32-битными вместо 16-битных - конечно, при этом немного увеличивается размер данных, зато существенно уменьшается размер и время выполнения кода. ============================================================================== === Hемного о теории "плагинописания" ======================================== ============================================================================== Замечаний нет. ============================================================================== === Модификация ядра Kolibri OS. Часть 1. Добавление новых функций в ядро ==== ============================================================================== Для того, что бы вычислить физический адрес приложения вызвавшего нашу функцию, следует проделать следующее: mov esi,[0x3010] ; Помещаем в EAX адрес таблицы приложения mov edi,[esi+0x10] ; Помещаем в EDI физический адрес начала ; приложения в памяти Замечание 1. Это работает, но унаследовано от давних времён. Сейчас всё проще: ; допустим, в eax user-mode указатель (на данные программы) add eax, std_application_base_address ; теперь в eax kernel-mode указатель. Можно по нему обращаться. ============================================================================== === Модификация ядра Kolibri OS. Часть 2. Изменяем существующие функции ====== ============================================================================== Потом, я подумал, "зачем городить костыли в каждой программе? не лучше ли будет, добавить эту функцию в ядро системы, чтобы любая программа могла обращаться к ней. Это избавит программиста от лишних заморочек и позволит сократить код приложения". Сказано - сделано. Замечание 1. Не будем здесь спорить о выбранном способе - споры ведутся в теме "Документация" на нашем форуме. По поводу сокращения кода приложения - ошибка: при существующей реализации размер кода не изменяется (техинфо: в esi всё равно надо передавать что-то (-1)), а размер данных увеличивается на нуль-терминатор. Меня тут упрекали в том, что это слишком тормозно (Lrz, привет ещё раз ;), неоптимально. Да, согласен, вывод строки с заранее неизвестной длиной, будет немного помедленнее, чем обычной. Но на современных мощностях компьютеров, это заметить практически невозможно. Замечание 2. В процессе вывода просматривается вся строка, так что остаётся только вставить проверку на нулевой символ в процессе просмотра. Так что при грамотной реализации (если не просматривать строку лишний раз для выяснения длины) это заметить вообще невозможно не только на современных мощностях, но и на отошедших в историю 386-х и 486-х. Замечание 3. В статье рассматривается именно способ с предвычислением длины. Тем не менее при изменении ядра на svn-сервере строка лишний раз не просматривается ... но любой встретившийся нулевой символ прекращает вывод (даже если программист специально захотел его вывести). Дальнейшие обсуждения на форуме. ============================================================================== === Программирование сокетов под КолибриОС. Часть 1 ========================= ============================================================================== Тут грамотных замечаний я сделать не могу, поскольку мало что понимаю в этой области... Тем не менее: * наряду с stack_ru.txt рекомендуется заглядывать в sysfuncr.txt - в первом документе описаны далеко не все функции * UDP-сокет закрывается без проблем. Приколы с "не захочет закрываться" начинаются с TCP-сокетами. По крайней мере, в текущей реализации ядра * Не пишите 'mov ebx,0'. Более эффективный (и по размеру, и по скорости) вариант 'xor ebx,ebx'. Если вы подключаете macros.inc и не заморачиваетесь с 'purge mov', то такая замена делается автоматически. Единственный случай, когда 'mov ebx,0' писать всё-таки можно и нужно - это если вам нужно не изменять регистр флагов eflags - mov его не портит, чего не скажешь о xor. * Не пишите 'cmp eax,0'. Более эффективный (и по размеру, и по скорости) вариант 'test eax,eax'.