Установка расширений PECL

Введение в установку PECL

» PECL — это репозиторий расширений PHP, которые доступны вам через систему » PEAR. Эта часть руководства предназначена для демонстрации того, как вы можете получить и установить расширения PECL.

Эти инструкции подразумевают, что /your/phpsrcdir/ является путем к каталогу с дистрибутивом исходного кода PHP, а extname — это имя расширения PECL. Поэтому, приведем в соответствие. Эти инструкции также подразумевают знакомство с » командой pear. Информация в руководстве PEAR для команды pear также применима для команды pecl.

Для того, чтобы расширение можно было использовать, оно должно быть собрано, установлено и загружено. Методы, описанные ниже, предоставляют вам различные рекомендации по поводу того, как собрать и установить расширения, но сами они не будут автоматически загружены. Расширения могут быть загружены через добавление директивы extension в файл php.ini, или путем использования функции dl().

В процессе сборки модулей PHP важно, чтобы у вас были правильные версии необходимых утилит (autoconf, automake, libtool и т.д.). Информацию об этих утилитах и их версиях можно посмотреть тут » Anonymous CVS Instructions.

Загрузка расширений PECL

Есть несколько вариантов для загрузки расширений PECL, в том числе:

  • » http://pecl.php.net/Вебсайт PECL содержит информацию о различных расширениях, которые предоставлены PHP Development Team. Информация, доступная на этом вебсайте, включает в себя: лог изменений, новости релизов, требования и другие подобные детали.
  • pecl download extnameРасширения PECL, которые были опубликованы на сайте PECL, доступны для скачивания и установки через » команду pecl. Также, могут быть также указаны особые ревизии.
  • CVSБольшинство расширений PECL, также, находятся в CVS. Веб-интерфейс для просмотра доступен по адресу » http://cvs.php.net/pecl/. Для загрузки напрямую из CVS используется следующая последовательность команд. Стоит отметить, что пользователь cvsread имеет пароль phpfi:
    $ cvs -d:pserver:cvsread@cvs.php.net:/repository login
    $ cvs -d:pserver:cvsread@cvs.php.net:/repository co pecl/extname
  • Загрузка для Windows Пользователи Windows могут найти скомпилированные расширения PECL путем скачивания Collection of PECL modules со страницы » PHP Downloads, либо через » PECL Snapshot, либо расширения DLL на странице » PECL4WIN. Для компиляции PHP под Windows прочитайте соответствующий раздел
  • PECL для пользователей Windows

    Как и в случае DLL расширения PHP, установка проста и заключается в копировании файла DLL расширения PECL в директориюextension_dir и подключением затем его через php.ini. Например, добавьте следующую строку в ваш php.ini:

    extension=php_extname.dll

    После выполнения этих действий, перезапустите web-сервис.

    Компиляция общих расширений с помощью команды pecl

    PECL позволяет легко создавать общие расширения PHP. Используя » команду pecl, выполните следующее:

    $ pecl install extname

    Эта команда загрузит исходный код для расширения extname, скомпилирует и установит extname.so в вашу директорию extension_dir. Файл extname.so может быть затем загружен в php.ini

    По умолчанию, команда pecl не будет устанавливать пакеты, отмеченные состоянием alpha или beta. Если нет доступных стабильных версий пакетов, вы можете установить beta версию пакета, используя следующую команду:

    $ pecl install extname-beta

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

    $ pecl install extname-0.1

    Замечание: После подключения расширения в php.ini необходимо перезапустить web-сервис для того, чтобы изменения вступили в силу.

    Компиляция общих расширений с помощью phpize

    Иногда, использование инсталятора pecl не подходит. Это может быть связано с тем, что вы находитесь за файерволом или из-за того, что расширение, которое вы хотите установить, недоступно в PECL-совместимом пакете (к примеру, расширения из CVS, которые еще не зарелизены). Если вам необходимо собрать такое расширение, вы можете использовать низкоуровневые утилиты для выполнения сборки вручную.

    Команда phpize используется для подготовки окружения для расширения PHP. В следующем примере директория, где находятся исходные коды расширения, называется extname:

    $ cd extname
    $ phpize
    $ ./configure
    $ make
    # make install

    В случае успешной установки будет создан файл extname.so и помещен в PHP extensions directory. Вам будет необходимо добавить строку extension=extname.so в php.ini перед тем, как вы станете использовать это расширение.

    Если в системе отсутствует phpize, но существует возможность установки заранее скомпилированных пакетов (типа RPM), убедитесь, что установлена соответствующая devel версия пакета PHP, так как они часто содержат команду phpize с подходящими файлами заголовоков для сборки PHP и его расширений.

    Используйте phpize —help для просмотра дополнительной информации.

    Компиляция расширений PECL статически в PHP

    Возможно, вы захотите собрать расширение PECL статично в ваш бинарйный файл PHP. Для этого необходимо поместить код расширения в директорию php-src/ext/ и вызвать перегенерацию конфигурационных скриптов через систему сборки PHP.

    $ cd /your/phpsrcdir/ext
    $ pecl download extname
    $ gzip -d < extname.tgz | tar -xvf -
    $ mv extname-x.x.x extname

    В результате будет создана следующая директория:

        /your/phpsrcdir/ext/extname

    После этого, выполните заново сборку конфигурационного скрипта PHP и затем соберите PHP как обычно:

    $ cd /your/phpsrcdir
    $ rm configure
    $ ./buildconf --force
    $ ./configure --help
    $ ./configure --with-extname --enable-someotherext --with-foobar
    $ make
    $ make install

    Замечание: Для запуска скрипта ‘buildconf’ вам необходимы autoconf версии 2.13 и automake версии 1.4+ (более новые версии autoconf могут работать, но это не поддерживается).

    Одна из двух опций —enable-extname или —with-extname используется в зависимости от расширения. Обычно, в случае, когда расширение не требует подключения внешних библиотек, используется —enable. Для того, чтобы убедиться в этом, выполните следующую команду после buildconf:

    $ ./configure --help | grep extname

    Клонирование дисков во FreeBSD

    Любой из нас хорошенько задумывается над тем, как правильно разбить HDD при установке FreeBSD. Действительно, потом будет весьма проблематично изменить размер патриции при необходимости. Проблема заключается в том, что на жестком диске находятся, так называемые, слайсы, а уже в них инкапсулированы партиции. Это не всегда так, потому что есть еще и второй метод разметки HDD без слайсов, но в данной статье он не рассматривается. Популярные программы для работы с разделами HDD, такие как Partition Magic, Acronis могут удалить слайс, копировать/переместить его посекторно, но никак не заглянуть внутрь и изменить размер той или иной партиции.

    Итак, есть система (далее, система — это установленная ОС FreeBSD, включающая в себя MBR, корневой раздел, SWAP,  ополнительные разделы /tmp, /var и т.д.) на жестком диске объемом 80 ГБ. Задача: перенести систему на другой жесткий диск объемом 250 ГБ, увеличивая размеры партиций пропорционально увеличению объема HDD. В нашем случае это 250-80=170 ГБ. Вторая часть задачи: повторять эту процедуру автоматически, с определенным интервалом (для создания резервной копии системы). В обоих случаях, нужно скопировать систему полностью, т.е. не только файлы и каталоги, а и загрузочный сектор, загрузчик для сохранения возможности загрузки системы с полученной копии.

    Для выполнения задачи воспользуемся утилиткой CloneHDD. Взять ее можно здесь: http://sourceforge.net/projects/clonehdd Качаем дистрибутив последней версии и выполняем:

            tar -zxvf clonehdd-2.0.3.tar.gz -C /home/user/

    Можно сделать проще — установить из портов:

            make install -C /usr/ports/sysutils/clonehdd

    После установки мы видим грозное предупреждение о том, что запуская clonehdd нужно семь раз отмерить а потом уже отрезать =)) Но все не так страшно, как кажется: программа изначально рассчитана так, что данные на исходном винте никогда и ни при каких условиях не подвергнутся записи.
    Это сообщение больше относится к нашему новому, чистому винту. Поэтому, если вы купили чистый винт и хотите на него переехать, то вряд ли clonehdd чем то сможет навредить.

    Работа с программой предельно проста. Для осуществления задуманного нам нужно всего один раз запустить утилиту с нужными параметрами. Вот о параметрах теперь поподробнее.

    -src=[device] Это исходный девайс на котором находится наша система. Чтобы узнать имя текущего винта, нужно выполнить команду «mount». Мы увидим таблицу, в первой колонке которой указаны имена примонтированных устройств. Вот, например, «/dev/ad0s1f». «/dev/» это папка, в которой находятся все устройства, «ad0» это нужное нам имя самого винта, «s1» слайс под номером 1, «f» имя партиции на слайсе. Вывод: значение этому параметру присваиваем «ad0»

    -dst=[device] Это девайс назначения. Именно сюда переедет ваша система. Узнать его можно, например, командой «cat /var/run/dmesg.boot». На экране появится список сообщений ядра при загрузке. Обычно, сообщения о найденных винтах находятся в самом конце. Ни одна партиция не должна быть примонтирована с этого винта, иначе CloneHDD выдасть ошибку.

    -swap=[size in MB] Размер будущего раздела SWAP.

    -freespace=[Required free space on dst partition] Если клонирование производится на винт меньшего размера, на DST винте после клонирования должен остаться небольшой кусочек свободного места, для того чтобы винт небыл на 100% заполнен после клонирования. По умолчанию — это 100МБ. Этим параметром его можно изменить.

    -safe [Required safe mode for `dump`] Включает режим безопасного копирования. В этом режиме сначала создается образ каждого раздела в папке .snap, а потом делается перенос. В «небезопасном» режиме, копирование производится на лету. Вся проблема в том что размер этого образа равен размеру данных на разделе. А т.к. образ будет записан на тот же раздел с которого производится копирование, на разделе долно быть минимум 50% свободного места. Вернемся к параметру -safe. Если параметр отсутствует, клонирование будет произведено в безопасном режиме если достаточно свободного места или в небезопасном режиме если этого места недостаточно. Если параметр задан, небезопасного копирования не будет, и
    разделы с недостатком свободного места склонированы не будут.

    -fstab=[Device name to write in fstab] После клонирования будет сгенерирован файл /etc/fstab на полученном винте. Параметр задает Имя девайса, который будет записан в этот файл. По умолчанию: девайс, заданный параметром -src.

    -force [Do not ask any questions] При выполнении второй части задачи, утилита будет выполняться из cron’а и нам не нужны лишние вопросы, в ходе выполнения программы. Данный параметр отключает интерактивный режим и вопрос «Are you sure?» задаваться не будет.

    Вернемся к нашей задаче. Мой исходный винт: ad0, новый винт: ad1.

            srv# clonehdd -src=ad0 -dst=ad1 -swap=1024
            Clone parameters:
            Source partition: /dev/ad0
            Dest partition: /dev/ad1
            Swap size: 1024 MB
            Safe dumping: Disabled
            Free space on DST: 100 MB
            Fstab device name: ad0
            ---
            [OK] Found devices for clone procedure
            [OK] DST partition is not in use
            ---
            Source partition
            /usr size: 64489MB, used: 10563MB
            /var size: 4958MB, used: 144MB
            / size: 1483MB, used: 82MB
            /tmp size: 1483MB, used: MB
            Total: 72415 MB, used: 10790 MB
            ---
            [OK] Device ad1 has enough free space
            Wait 10 seconds before start: 10 9 8 7 6 5 4 3 2 1
            [OK] Device /dev/ad1 made clean
            [OK] New slice created
            ---
            Destination device partitions:
            SWAP size: 1024 MB
            / size 1588 MB
            /tmp size 1588 MB
            /var size 5306 MB
            /usr size 69026 MB
            ---
            [INF] Last partition were increased for 3 blocks
            [OK] Partitions were created successfully
            ---
     
            [OK] Partition /tmp was formatted successfully
            Starting dump/restore procedure...	[OK]
     
            [OK] Partition /var was formatted successfully
            Starting dump/restore procedure...	[OK]
     
            [OK] Partition /usr was formatted successfully
            Starting dump/restore procedure...	[OK]
     
            [OK] Partition / was formatted successfully
            Starting dump/restore procedure...	[OK]
     
            [OK] file /etc/fstab generated successfully

    Видим сообщения об успешном клонировании. Теперь сделаем выполнение задачи по расписанию: выполнять каждый день в 2 часа ночи клонирование рабочего винта на запасной. Добавляем в файл /etc/crontab строку

            0 2 * * * root clonehdd -src=ad0 -dst=ad1 -swap=1024 -force >/dev/null

    Обратите внимание, что >/dev/null убивает не все сообщения. Только обычные сообщения будут опущены. Все сообщения об ошибках отсылаются на выход STDERR, не попадают на /dev/null и будут отправлены cron’ом по
    почте системному администратору.

    windows автоматический вход в систему

    При запуске windows отменить запрос логина/пароля, для этого выбираем пуск->выполнить-> набираем в окошке
    control userpasswords2 и ставим галочку не спрашивать логин/пароль при входе в систему.

    Изучаем параметры gcc

    Перевод статьи «Getting Familiar with GCC Parameters», автор — Mulyadi Santosa

    gcc (GNU C Compiler) — набор утилит для компиляции, ассемблирования и компоновки. Их целью является создание готового к запуску исполняемого файла в формате, понимаемом вашей ОС. Для Linux, этим форматом является ELF (Executable and Linking Format) на x86 (32- и 64-битных). Но знаете ли вы, что могут сделать для вас некоторые параметры gcc? Если вы ищете способы оптимизации получаемого бинарного файла, подготовки сессии отладки или просто наблюдать за действиями, предпринимаемыми gcc для превращения вашего исходного кода в исполняемый файл, то знакомство с этими параметрами обязательно. Так что, читайте.

    Напомню, что gcc делает несколько шагов, а не только один. Вот небольшое объяснение их смысла:

    Препроцессирование: Создание кода, более не содержащего директив. Вещи вроде «#if» не могут быть поняты компилятором, поэтому должны быть переведены в реальный код. Также на этой стадии разворачиваются макросы, делая итоговый код больше, чем оригинальный. [1]

    Компиляция: Берется обработанный код, проводятся лексический и синтаксический анализы, и генерируется ассемблерный код. В течение этой фазы, gcc выдает сообщения об ошибках или предупреждениях в случае, если анализатор при парсинге вашего кода находит там какие-либо ошибки. Если запрашивается оптимизация, gcc продолжит анализировать ваш код в поисках улучшений и манипулировать с ними дальнейшем. Эта работа происходит в многопроходном стиле, что показывает то, что иногда требуется больше одного прохода по коду для оптимизации. [2]

    Ассемблирование: Принимаются ассемблерные мнемоники и производятся объектные коды, содержащие коды команд. Часто недопонимают то, что на стадии компиляции не производятся коды команд, это делается на стадии ассемблирования. В результате получаются один или более объектных файла, содержащие коды команд, которые являются действительно машинозависимыми. [3]

    Компоновка: Трансформирует объектные файлы в итоговые исполняемые. Одних только кодов операции недостаточно для того, чтобы операционная система распознала и выполнила их. Они должны быть встроены в более полную форму. Эта форма, известная как бинарный формат, указывает, как ОС загружает бинарный файл, компонует перемещение и делает другую необходимую работу. ELF является форматом по умолчанию для Linux на x86. [4]

    Параметры gcc описаны здесь, прямо и косвенно затрагивая все четыре стадии, поэтому для ясности, эта статья построена следующим образом:

    — Параметры, относящиеся к оптимизации

    — Параметры, относящиеся к вызову функций

    — Параметры, относящиеся к отладке

    — Параметры, относящиеся к препроцессированию

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

    — Коллекция утилит ELF, которая включает в себя такие программы, как objdump и readelf. Они парсят для нас информацию о ELF.

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

    — time, простой способ узнать общее время работы программы.

    Следующие инструкции могут быть применены в gcc версий 3.x и 4.x, так что они достаточно общие. Начнем копать?

    Параметры, относящиеся к оптимизации кода

    .

    gcc предоставляет очень простой способ производить оптимизацию: опция -O. Она и ускоряет выполнение вашего кода, и сжимает размер итогового кода. У неё существует пять вариантов:

    — от -O0 (O ноль) до -O3. «0» означает отсутствие оптимизации, а «3» — наивысший уровень оптимизации. «1» и «2» находятся между этими краями. Если просто используете -O без указания номера, это будет означать -O1.

    — -Os говорит gcc оптимизировать размер. В общем-то, это похоже на -O2, но пропускает несколько шагов, которые могут увеличить размер.

    Какое ускорение в действительности можно от них получить? Что ж, предположим, у нас есть такой код:

    #include 
     
    int main(int argc, char *argv[])
     
    {
     
       int i,j,k;
     
       unsigned long acc=0; 
     
       for(i=0;i<10000;i++)
     
            for(j=0;j<5000;j++)
     
                    for(k=0;k<4;k++)
     
                            acc+=k;
     
       printf("acc = %lun",acc);
     
       return 0;
     
    }

    С помощью gcc, создадутся четыре разных бинарных файла, используя каждый из -O вариантов (кроме -Os). Утилита time запишет их время исполнения, таким образом:

    $ time ./non-optimized
    Без оптимизации -O1 -O2 -O3
    real 0.728 0.1 0.1 0.1
    user 0.728 0.097 0.1 0.1
    sys 0.000 0.002 0.000 0.000

    Для упрощения, будем использовать следующие обозначения:

    Non-optimized обозначает исполняемый файл, скомпилированный с опцией -O0.

    OptimizedO1 обозначает исполняемый файл, скомпилированный с опцией -O1.

    OptimizedO2 обозначает исполняемый файл, скомпилированный с опцией -O2.

    OptimizedO3 обозначает исполняемый файл, скомпилированный с опцией -O3.

    Как вы могли заметить, время выполнения программы, скомпилированной с -O1 в семь раз меньше, чем время выполнения программы, при компиляции которой не использовалась оптимизация. Обратите внимание, что нет большой разницы между -O1, -O2 и -O3, — на самом деле, они почти одинаковы. Так в чем же магия -O1?

    После беглого изучения исходного кода, вы должны отметить, что такой код конечен для оптимизации. Прежде всего, давайте посмотрим на короткое сравнение дизассемблированных версий non-optimized и optimizedO1:

    $ objdump -D non-optimized
     
    $ objdump -D optimizedO1

    (Примечание: вы можете получить другие результаты, поэтому используйте эти как основные)

    Non-optimized OptimizedO1
    mov 0xfffffff4(%ebp)

    add %eax,0xfffffff8(%ebp)

    addl $0x1,0xfffffff4(%ebp)

    cmpl $0x3,0xfffffff4(%ebp)

    add $0x6,%edx

    add $0x1,%eax

    cmp $0x1388,%eax

    Приведенные примеры реализуют самый вложенный цикл (for (k=0;k<4;k++)). Обратите внимание на различие: неоптимизированный код напрямую загружает и хранит из адреса памяти, в то время как optimized01 использует регистры ЦПУ в качестве сумматора и счетчик цикла. Как вам, возможно, известно, доступ к регистрам может быть получен в сотни или тысячи раз быстрее, чем к ячейкам ОЗУ.

    Не удовлетворяясь простым использованием регистров ЦПУ, gcc использует другой трюк оптимизации. Давайте снова посмотрим дизассемблированный код optimizedO1 и обратим внимание на функцию main():

    ......
     
       08048390 :
     
       ...
     
       80483a1:       b9 00 00 00 00          mov    $0x0,%ecx
     
       80483a6:       eb 1f                   jmp    80483c7 
     
       80483a8:       81 c1 30 75 00 00       add    $0x7530,%ecx

    0x7530 это 30000 в десятичной форме, поэтому мы можем быстро угадать цикл. Этот код представляет собой самый вложенный и самый внешний циклы (for(j=0;j<5000;j++) … for(k=0;k<4;k++)), так как они являются буквальным запросом на 30000 проходов. Примите во внимание, что вам нужно всего лишь три прохода внутри. Когда k=0, acc остается прежним, поэтому первый проход можно проигнорировать.

       80483ae:       81 f9 00 a3 e1 11       cmp    $0x11e1a300,%ecx
     
       80483b4:       74 1a                   je     80483d0 
     
       80483b6:       eb 0f                   jmp    80483c7

    Хмм, теперь это соответствует 300 000 000 (10 000*5 000*6). Представлены все три цикла. После достижения этого числа проходов, мы переходим прямо к printf() для вывода суммы (адреса 0x80483d0 — 0x80483db).

       80483b8:       83 c2 06                add    $0x6,%edx
     
       80483bb:       83 c0 01                add    $0x1,%eax
     
       80483be:       3d 88 13 00 00          cmp    $0x1388,%eax
     
       80483c3:       74 e3                   je     80483a8 
     
       80483c5:       eb f1                   jmp    80483b8

    Шесть добавляется в сумматор при каждой итерации. В итоге, %edx будет содержать всю сумму после выполнения всех трех циклов. Третья и четвертая строки показывают нам, что после выполнения 5000 раз, должен быть переход к адресу 0x80483a8 (как указано ранее).

    Мы можем заключить, что gcc создает здесь упрощение. Вместо прохода три раза в самый вложенный цикл, он просто добавляет шесть для каждого среднего цикла. Это звучит просто, но это заставляет вашу программу сделать только 100 000 000 проходов вместо 300 000 000. Это упрощение, называемое разворачиванием цикла, одно из тех задач, которые делают -O1/2/3. Конечно же, вы и сами можете это сделать, но иногда неплохо знать, что gcc может определить такие вещи и оптимизировать их.

    С опциями -O2 и -O3 gcc тоже пытается произвести оптимизацию. Обычно она достигается посредством переупорядочивания [5] и трансформацией кода. Целью этой процедуры является устранить столько ошибочных ветвей, сколько возможно, что повышает качество использования конвейера. Например, мы можем сравнить, как non-optimized и optimizedO2 выполняет самый внешний цикл.

     80483d4:       83 45 ec 01             addl   $0x1,0xffffffec(%ebp)
     
     80483d8:       81 7d ec 0f 27 00 00    cmpl   $0x270f,0xffffffec(%ebp)
     
     80483df:       7e c4                   jle    80483a5

    Бинарный файл non-optimized использует jle для выполнения перехода. Математически это означает, что вероятность выбора ветви 50%. С другой стороны, версия optimizedO2 использует следующее:

    80483b4:       81 c1 30 75 00 00       add    $0x7530,%ecx
     
    80483ba:       81 f9 00 a3 e1 11       cmp    $0x11e1a300,%ecx
     
    80483c0:       75 e1                   jne    80483a3

    Теперь, вместо jle используется jne. При условии, что любое целое может быть сопоставлено в предыдущем cmp, нетрудно сделать вывод, что это увеличит шанс выбора ветви почти до 100%. Это небольшое, но полезное указание процессору для определения того, какой код должен быть выполнен. Хотя, для большинства современных процессоров, этот вид трансформации не является ужасно необходимым, так как предсказатель переходов достаточно умен для того, чтобы сделать это сам.

    Для доказательства того, как сильно это изменение может помощь, к нам на помощь придет OProfile. Oprofile выполнен для записи числа изолированных ветвей и изолированных ошибочных ветвей. Изолированные здесь обозначает «выполненные внутри конвейера данных ЦПУ»

    $ opcontrol --event=RETIRED_BRANCHES_MISPREDICTED:1000 --event=RETIRED_BRANCHES:1000;

    Мы запустим non-optimized и optimizedO2 пять раз каждый. Затем мы возьмем максимум и минимум примеров. Мы посчитаем степень ошибки, используя эту формулу (выведена отсюда).

    Степень ошибки = изолированные ошибочные ветви / изолированные ветви

    Теперь вычислим степень ошибки для каждого бинарного файла. Для non-optimized получилось 0,5117%, в то время как optimizedO2 получил 0,4323% — в нашем случае, выгода очень мала. Фактическая выгода может различаться для реальных случаев, так как gcc сам по себе не может много сделать без внешних указаний. Пожалуйста, прочтите о __builtin_expect() в документации по gcc для подробной информации.

    Параметры, относящиеся к вызову функций

    По существу, gcc предоставляет вам несколько путей управления тем, как вызывается функция. Сначала давайте рассмотрим встраивание. С помощью встраивания, вы сокращаете стоимость вызова функции, так как тело функции подставлено прямо в вызывающую функцию. Пожалуйста, учтите, что это не по умолчанию, а только когда вы используете -O3 или, по крайней мере, -finline-functions.

    Как полученный бинарный файл выглядит после того, как gcc сделает встраивание? Рассмотрим следующий листинг:

    #include 
     
    inline test(int a, int b, int c)
     
    {
     
            int d;
     
            d=a*b*c;
     
            printf("%d * %d * %d is %dn",a,b,c,d);
     
    }
     
    static inline test2(int a, int b, int c)
     
    {
     
             int d;
     
             d=a+b+c;
     
             printf("%d + %d + %d is %dn",a,b,c,d);
     
    }
     
    int main(int argc, char *argv[])
     
    {
     
            test(1,2,3);
     
            test2(4,5,6);
     
    }

    Скомпилируем этот код со следующим параметром:

    $ gcc -S -O3 -o

    -S указывает gcc остановиться сразу после стадии компиляции (мы расскажем о ней позже в этой статье). Результат будет следующим:

    ....
     
    test:
     
            pushl   %ebp
     
            movl    %esp, %ebp
     
            pushl   %ebx
     
    ....
     
    main:
     
            leal    4(%esp), %ecx
     
            andl    $-16, %esp
     
            pushl   -4(%ecx)
     
    ...
     
            movl    $6, 16(%esp)
     
            movl    $3, 12(%esp)
     
            movl    $2, 8(%esp)
     
            movl    $1, 4(%esp)
     
            movl    $.LC0, (%esp)
     
            call    printf
     
    ...
     
            movl    $15, 16(%esp)
     
            movl    $6, 12(%esp)
     
            movl    $5, 8(%esp)
     
            movl    $4, 4(%esp)
     
            movl    $.LC1, (%esp)
     
            call    printf
     
    ...

    И test(), и test() действительно встроены, но вы также можете видеть test(), который остался вне main(). Вот где играет роль ключевое слово static. Написав, что функция — static, вы сообщаете gcc, что эта функция не будет вызываться из какого-либо внешнего объектного файла, поэтому нет нужды порождать коды. Таким образом, это экономит размер, и если вы можете сделать функцию статичной, сделайте это где только возможно. С другой стороны, будьте благоразумны при решении, какая функция должна быть встраиваемой. Увеличение размера для небольшого увеличения скорости не всегда оправдано.

    С помощью некоторой эвристики, gcc решает, должна быть функция встраиваемой, или нет. Одним из таких доводов является размер функции в терминах псевдо-инструкций. По умолчанию, лимитом является 600. Вы можете поменять этот лимит, используя -finline-limit. Проэксперементируйте для нахождения лучших лимитов встраивания для вашего конкретного случая. Также возможно переделать эвристику так, чтобы gcc всегда встраивал функцию. Просто объявите вашу функцию так:

    __attribute__((always_inline)) static inline test(int a, int b, int c)

    Теперь перейдем к передаче параметров. На архитектуре x86, параметры помещаются в стек и позже достаются из стека для дальнейшей обработки. Но gcc дает вам возможность изменить это поведение и использовать вместо этого регистры. Функции, у которых меньше трех параметров могут использовать эту возможность указанием -mregparm=, где n — число регистров, которое вы хотите использовать. Если мы применим этот параметр (n=3) к предыдущему коду, убрав атрибут inline и не используя оптимизацию, мы получим:

    ...
     
    test:
     
            pushl   %ebp
     
            movl    %esp, %ebp
     
            subl    $56, %esp
     
            movl    %eax, -20(%ebp)
     
            movl    %edx, -24(%ebp)
     
            movl    %ecx, -28(%ebp)
     
    ...
     
    main:
     
    ...
     
            movl    $3, %ecx
     
            movl    $2, %edx
     
            movl    $1, %eax
     
            call    test

    Вместо стека, используются EAX, EDX и ECX для хранения первого, второго и третьего параметров. Поскольку доступ к регистру происходит быстрее, чем к ОЗУ, это будет одним из способов уменьшить время работы. Хотя вы должны обратить внимание на следующие вещи:

    — Вы ДОЛЖНЫ компилировать весь ваш код с таким же числом -mregparm регистров. Иначе у вас будут проблемы с вызовом функций из другого объектного файла, если они будут принимать разные соглашения.

    — Используя -mregparm, вы разрушаете совместимый с Intel x86 бинарный интерфейс приложений (ABI). Поэтому, вы должны учитывать это, если вы распространяете свое ПО только в бинарной форме.

    Возможно, вы заметили эту последовательность в начале каждой функции:

    push   %ebp
     
    mov    %esp,%ebp
     
    sub    $0x28,%esp

    Эта последовательность, также известная как пролог функции, написана чтобы установить указатель фрейма (EBP). Это приносит пользу, помогая отладчику делать трассировку стека. Следующая структура поможет вам понять это [6]:

    [ebp-01] Последний байт последней локальной переменной

    [ebp+00] Старое значение ebp

    [ebp+04] Возвращает адрес

    [ebp+08] Первый аргумент

    Можем мы пренебречь этим? Да, с помощью -fomit-frame-pointer, пролог будет укорочен, так что функция начнется просто с выделения стека (если есть локальные переменные):

    sub    $0x28,%esp

    Если функция вызывается очень часто, вырезание пролога спасет несколько тактов ЦПУ. Но будьте осторожны: делая это, вы также усложняете отладчику задачу по изучению стека. Например, давайте добавим test(7,7,7) в конец test2() и перекомпилируем с параметром -fomit-frame-pointer и без оптимизации. Теперь запустите gdb для исследования бинарного файла:

    $ gdb inline
     
    (gdb) break test
     
    (gdb) r
     
    Breakpoint 1, 0x08048384 in test ()
     
    (gdb) cont
     
    Breakpoint 1, 0x08048384 in test ()
     
    (gdb) bt
     
    #0  0x08048384 in test ()
     
    #1  0x08048424 in test2 ()
     
    #2  0x00000007 in ?? ()
     
    #3  0x00000007 in ?? ()
     
    #4  0x00000007 in ?? ()
     
    #5  0x00000006 in ?? ()
     
    #6  0x0000000f in ?? ()
     
    #7  0x00000000 in ?? ()

    При втором вызове test, программа остановилась, и gdb вывел трассировку стека. В нормальной ситуации, main() должна идти в фрейме №2, но мы видим только знаки вопроса. Запомните, что я сказал про расположение стека: отсутствие указателя фрейма мешает gdb находить расположение сохраненного возвращаемого адреса в фрейме №2.

    Опции, относящиеся к отладке.

    Каждый иногда нуждается в отладке его или её кода. Когда это время приходит, обычно вы запускаете gdb, ставите точки останова там и тут, анализируете бэктрейсы, и так далее, чтобы выявить расположение нарушающего работу кода. А что получаете на самом деле? Если вы не используете опции отладки, вы просто получаете адрес, указывающий на регистр EIP.

    Вот в чем проблема, в действительности вы не хотите адрес. Вы хотите, чтобы gdb или другой отладчик просто показал требуемые строки. Но gdb не может этого сделать без определенного вида помощи. Эта помощь, называемая отладкой с приписываемыми форматами записи (Debugging With Attributed Record Formats — DWARF), помогает вам выполнять отладку на уровне исходного кода.

    Как это сделать? Используйте -g при компиляции в объектный код, то есть:

      gcc -o -g test test.c

    Что такого добавляет gcc, что отладчик может сопоставлять адрес с исходным кодом? Вам нужна dwarfdump [7] чтобы узнать это. Это утилита находится внутри тарболла libdwarf или в RPM, так что вы не найдете её в виде отдельного пакета. Скомпилируйте её сами или просто установите из репозитория вашего дистрибутива; оба варианта должны сработать. В этой части статьи я использую версию 20060614 RPM.

    Используя readelf, вы можете отметить, что в неотлаженной версии первого листинга существует 28 разделов:

     $ readelf -S ./non-optimized

    Но в отлаженной версий есть 36 разделов. Новые разделы:

    * debug_arranges

    * debug_pubnames

    * debug_info

    * debug_abbrev

    * debug_line

    * debug_frame

    * debug_str

    * debug_loc

    Вам не нужно копаться во всех этих разделах; для быстрого изучения, будет достаточно рассмотреть .debug_line. Команда, которая вам нужна:

    $ /path/to/dwarfdump -l <object width="100" height="100" type="application/x-shockwave-flash"></object>

    Вот пример того, что вы получите:

     .debug_line: line number info for a single cu
     
       Source lines (from CU-DIE at .debug_info offset 11):
     
             [row,column]
        //
     
     /code/./non-optimized.c:  [  3,-1]        0x8048384       // new statement
     
     /code/./non-optimized.c:  [  5,-1]        0x8048395       // new statement
     
     ...............

    Интерпретация этих сообщений довольно простая. Возьмите первую запись (идущую за строкой ) в качестве примера:

      line number 3 in file non-optimized.c is located in address 0x8048384.

    gdb дает ту же информацию:

     $ gdb non-optimized-debugging
     
       (gdb) l *0x8048384
     
       0x8048384 is in main (./non-optimized.c:3).

    readelf также предоставляет похожую информацию, используя —debug-info:

     $ readelf --debug-dump=line <object width="100" height="100" type="application/x-shockwave-flash"></object>

    И readelf, и dwarfdump могут анализировать информацию отладки, так что вы вольны выбирать сами.

    Что вы должны понимать, так это то, что исходный код сам по себе не встроен в объектный файл. На самом деле, отладчик должен проверять отдельный файл исходного кода. Запись в колонке помогает определять, откуда загружать файл исходного кода. Учтите, что она содержит полный путь, что значит невозможность его нахождения gdb в случае перемещения или переименования.

    gcc сам имеет возможность давать много информации об отладке. Кроме DWARF, существуют:

    * Stabs: -gstabs производит собственный формат stabs, в то время, как -gstabs+ включает в себя специфичные расширения GNU.

    * Common Object File Format (COFF): Создается с помощью -gcoff.

    * XCOFF: Создается с помощью -gxcoff. Если вы предпочитаете включать расширения GNU, используйте -gxcoff+.

    * Virtual Memory System (VMS): Создается с помощью -gvms.

    Каждый из этих форматов описаны в ссылках ([8], [9] и [10]), но для x86-совместимой архитектуры, без проблем, вам лучше использовать формат DWARF. Последней спецификацией DWARF является DWARF третьей версии, и gdb может создавать её с помощью -gdwarf-2. Это может ввести в заблуждение новых пользователей, так как вы можете подумать, что такая опция создаст информацию DWARF 2. На самом деле, DWARF 2 объединен с некоторыми возможностями DWARF 3. Не каждый отладчик поддерживает третью версию, поэтому используйте её с осторожностью.

    Хотя не всегда все идет так гладко. Когда вы комбинируете опции -O и -g, для информации о строке необходимо установить связь с фактическим кодом в упомянутом сдвиге адреса. Пример может это пояснить. Возьмите файл (я снова использую первый листинг) и скомпилируйте его:

     $ gcc -O2 -ggdb -o debug-optimized listing-one.c
     
       $ readelf --debug-dump=line debug-optimized
     
       ..
     
       Special opcode 107: advance Address by 7 to 0x80483aa and Line by 4 to 11
     
       ...

    Но что говорит gdb?

     $ gdb debug-optimized
     
       (gdb) l *0x80483aa
     
       0x80483aa is in main (./non-optimized.c:11).
     
       ...
     
       11              printf("acc = %lun",acc);
     
       ...
     
       (gdb) disassemble main
     
       ...
     
       0x080483aa :   add    $0x6,%edx
     
       0x080483ad :   cmp    $0x1388,%eax
     
       ...

    Здесь вы видите полное расхождение. Изучая одну информацию об отладке, вы будете ожидать, что указанный адрес содержит что-то вроде инструкции CALL. Но в действительности, вы получите инструкции ADD и CMP, что больше похоже на конструкцию цикла. Это побочный эффект действий оптимизации — в этом случае меняется порядок инструкций. Так что возьмите себе за правило не смешивать опции -g (или её варианты) c -O.

    Опции, управляющие стадиями компиляции.

    В целях изучения, иногда вы хотите узнать, как ваш исходный код трансформируется в исполняемый. К счастью, gcc предоставляет вам опции для остановки на любой стадии обработки. Вспомните, что gcc имеет несколько стадий завершения, — например, компоновку. Есть такие опции:

    * -c останавливает на стадии ассемблирования, но пропускает компоновку. Результатом является объектный код.

    * -E останавливает на стадии препроцессинга. Все директивы препроцессора развернуты, так что вы видите только чистый код.

    * -S останавливает после компиляции. Она оставляет вас с ассемблерным кодом.

    -c наиболее часто используется, когда у вас есть несколько исходных файлов и вы хотите скомбинировать их для получения итогового исполняемого файла. Так что, вместо такого:

    $ gcc -o final-binary test1.c test2.c

    будет лучше разделить их так:

    $ gcc -c -o test1.o test1.c
     
    $ gcc -c -o test2.o test2.c

    и затем:

    $ gcc -o final-binary ./test1.o ./test1.o

    Возможно, вы заметили, что такая же последовательность используется, если вы собираете программу, используя Makefile. Преимущество использования -c ясно: вам нужно перекомпилировать только измененные исходные файлы. Только фаза, на которой переделывается компоновка всех объектных файлов, и это очень экономит время, особенно в больших проектах. Очевидным примером этого является ядро Linux.

    -E будет полезна, если вы хотите посмотреть, как ваш код в действительности выглядит после разворачивания макросов, определений и т.п. Возьмите следующий листинг в качестве примера:

    #include 
     
    #define A 2
     
    #define B 4
     
    #define calculate(a,b) a*a + b*b
     
    void plain_dummy()
     
    {
     
        printf("Just a dummyn");
     
    }
     
    static inline justtest()
     
    {
     
        printf("Hi!n");
     
    }
     
    int main(int argc, char *argv[])
     
    {
     
    #ifdef TEST
     
        justtest();
     
    #endif
     
        printf("%dn", calculate(A,B));
     
        return 0;
     
    }

    Скомпилируем его следующим образом:

    $ gcc -E -o listing2.e listing2.c

    Учтите, что мы не указываем параметров -D, что означает, что TEST не определен. Так и что мы имеем в препроцессорном файле?

    void plain_dummy()
     
    {
     
        printf("Just a dummyn");
     
    }
     
    static inline justtest()
     
    {
     
        printf("Hi!n");
     
    }
     
    int main(int argc, char *argv[])
     
    {
     
        printf("%dn", 2*2 + 4*4);
     
        return 0;
     
    }

    А где вызов justtest() внутри main()? Нигде. TEST не определен — вот почему код исключен. Вы также можете видеть, что макрос calculate уже развернут в умножение и сложение констант. В конечной исполняемой форме, эти числа будут заменены результатами операций. Как вы видите, -E довольно удобна для проверки корректности директив.

    Обратите внимание, что plain_dummy() все ещё здесь, не смотря на то, что она ни разу не вызывается. Это не удивительно, так как ещё не была произведена компиляция, вот почему не произошло исключение «мертвого» кода на этой стадии. stdio.h также развернут, но не показан в листинге выше.

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

    -S дает вам код на ассемблере, больше похожий на то, что вы видите с помощью objdump -d/-D. Хотя с помощью -S вы продолжите видеть директивы и имена символов, который делают код проще к изучению. Например, вызов printf(«%dn», 20) может быть трансформирован в:

    .section        .rodata.str1.1,"aMS",@progbits,1
     
    .LC0:
     
            .string "%dn"
     
    ...
     
            movl    $20, 4(%esp)
     
            movl    $.LC0, (%esp)
     
            call    printf

    Вы можете видеть, что format string %d помещена в область данных, доступную только для чтения (.rodata). Также, вы можете удостовериться, что аргументы помещаются в стек справа налево, со строкой форматирования на верху стека.

    Заключение.

    gcc дает нам много полезных опций для превращения нашего кода во что угодно. Понимая, что эти опции делают на самом деле, мы можем сделать программу быстрее и ловчее. Хотя, не будьте целиком от них зависимы: вы должны больше внимания уделять написанию эффективного и хорошо структурированного кода.

    Благодарности.

    Я хочу поблагодарить сообщества на irc каналах OFTC (#kernelnewbies и #gcc) и #osdev (Freenode) за их полезные идеи.

    Ссылки

    1. Статья в Википедии о Препроцессоре;

    2. Статья в Википедии о Компиляторе;

    3. Статья в Википедии об Ассемблере;

    4. Статья в Википедии о Компоновщике.

    5. Пример переупорядочивания кода (англ.);

    6. Frame pointer omission (FPO) optimization and consequences when debugging, Часть 1 и Часть 2 (англ.);

    7. Описание DWARF (англ.);

    8. Описание stabs (англ.);

    9. Описание COFF (англ.);

    10. Описание XCOFF (варианта COFF) (англ.);

    11. Использование препроцессора C в качестве утилиты создания HTML (англ.);

    12. Документация по gcc (англ.);

    13. AMD Athlon Processor x86 Code Optimization Guide (англ., pdf).

    Обработка текстов в *nix

    Очень часто администраторам *nix-систем приходится выполнять различные выборки, сортировки, группировки неструктурированных данных. Благо система имеет уйму утилит для этого. Рассмотрим в примерах выборку определённых полей файла. Надо отобрать из файла данные размещённые в первой, пятой и шестой колонках файла /etc/passwd.

    Существует множество решений, ниже приведены лишь несколько из них:

       1. cat /etc/passwd | awk -F: '{print $1" "$5" "$6}'
     
          ...
          avahi Avahi mDNS daemon,,, /var/run/avahi-daemon
          haldaemon Hardware abstraction layer,,, /home/haldaemon
          ...
     
       2. cat /etc/passwd | awk -F: '{print $1":"$5":"$6}'
     
          ...
          avahi:Avahi mDNS daemon,,,:/var/run/avahi-daemon
          haldaemon:Hardware abstraction layer,,,:/home/haldaemon
          ...
     
       3. cat /etc/passwd | cut -d":" -f1,5,6
     
          ...
          avahi:Avahi mDNS daemon,,,:/var/run/avahi-daemon
          haldaemon:Hardware abstraction layer,,,:/home/haldaemon
          ...
     
       4. cut -d":" -f1,5,6 /etc/passwd
     
          ...
          avahi:Avahi mDNS daemon,,,:/var/run/avahi-daemon
          haldaemon:Hardware abstraction layer,,,:/home/haldaemon
          ...

    Чем отличаются друг от друга эти варианты?

    Первые два варианта: 1 и 2, — используют cat и awk. Первая утилита выводит содержимое файла /etc/passwd, вторая отбирает необходимые поля. «-F:» указывает на то, что разделителем полей в потоке служит двоеточие. Обратный слэш () перед двоеточием предписывает читать двоеточие, как двоеточие и не пытаться обработать его, как спецсимвол. В общем-то, обратный слэш используется в данном случае для перестраховки. Обратите внимание, что в первом случае мы вывели поля (нумеруются начиная с 1), разделённые пробелом, а во втором — двоеточием. Если вам необходимо модифицировать вывод, используйте эту связку, если вам нужно просто вывести требуемые поля пользуйтесь вариантами три или четыре.

    В третьем варианте используется связка cat и cut. Первый выводит, второй режет 🙂 Эту связку можно можифицировать так, как указано в варианте 4, так как cut может принимать на вход и поток (вариант три), и файл (вариант четыре). -d»:» — указывает считать разделителем полей двоеточие. Здесь разделитель указан в кавычках, поэтому использовать обратный слэш () нет необходимости. -f1,5,6 говорит о том, что нужно отобразить 1-ое, 5-ое и 6-ое поля. Имейте ввиду, что для cut порядок полей, пречисленных после -f, не имеет значения. Поля будут выводится так, как они расположены в файле. Другими словами, вы просто указываете какие поля вас интересуют, а не порядок вывода полей. Если вам необходимо изменить порядок полей, воспользуйтесь awk.Например, так:

    cat /etc/passwd | awk -F: '{print $6" "$5" "$1}'
     
    ...
    /var/run/avahi-daemon Avahi mDNS daemon,,, avahi
    /home/haldaemon Hardware abstraction layer,,, haldaemon
    ...

    Кроме того, awk также умеет получать данные не только из потока, но и напрямую из файла. Т.е. последний пример мы можем переписать так:

    awk -F: '{print $6" "$5" "$1}' /etc/passwd
     
    ...
    /var/run/avahi-daemon Avahi mDNS daemon,,, avahi
    /home/haldaemon Hardware abstraction layer,,, haldaemon
    ...

    Почему я чаще пользуюсь конструкцией «cat | awk» или «cat | cut»? Всё просто. Когда я пытаюсь отобрать поля, я не всегда помню их точное положение в файле. Тогда я делаю: cat /etc/passwd

    Получаю полный список. После чего, уточнив нужные поля, нажимаю стрелочку вверх (в bash это вызов предыдущей команды) и дописываю | awk……..