Прозрачный MC

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

К слову, пользователи KDE могут сделать это штатными следствами – в настройках Konsole, а для пользователей других WM такой способ просто недоступен.

Немного «погуглив», я нашел решение. В любом эдиторе (только не том, что входит в MC), например nano открываем файл ~/.mc/ini (в консоли это выглядит так: nano -w ~/.mc/ini). Идем в самый конец файла и дописываем там следующюю секцию:

[Colors]
base_color=normal=,default:selected=,:marked=,default:markselect=,:menu=,:menuhot=,:menusel=,:menuhotsel=,:dnormal=,:dfocus=,:dhotnormal=,:dhotfocus=,:input=,:reverse=,:executable=,default:directory=,default:link=,default:device=,default:special=,:core=,:helpnormal=,:helplink=,:helpslink=,:

Сохраняем изменения, закрываем, набираем mc и наслаждаемся видом «крутого» MC.

Очень полезное дополнение – MC будет прозрачным только под обычным юзером, от имени которого вы сделали эти изменения, если вы наберете su -c mc, то MC останется непрозрачным (в отличие от «прозрачности для МС» в KDE).

Очень удобно.

Пошаговая настройка SSL для Apache

Перевод статьи «Step by Step: Configuring SSL Under Apache» автора Juliet Kemp.

Вступление

Если у вас безопасный веб-сервер, пользователи, волнующиеся за безопасность своих данных, могут быть уверены, что запросы зашифрованы, поэтому их данные в безопасности. Лучшим путем для этого является использование Apache 2, лидирующего веб-сервера под Linux и Secure Sockets Layer, протокол безопасной передачи данных. Transport Layer Security (TLS) является развитием SSL, но они работают практически одинаково. Я буду ссылаться только на SSL.

SSL — протокол для безопасной передачи зашифрованных данных между веб-браузером и веб-сервером. В большинстве случаев, аутентификацию проходит сервер, что позволяет клиенту быть уверенным в том, что сервер является требуемым ему, а не наоборот. Как бы то ни было, когда соединение устанавливается, обе стороны находятся в безопасности, так как только клиент и сервер имеют доступ к ключу. Это работает в течении многих запросов, сервер не интересует, кем является клиент так долго, сколько он остается тем же клиентом на протяжении запроса. Если вас волнует аутентификация клиента, есть возможность использовать клиентские SSL сертификаты (или htaccess, Kerberos или другие, близкие к ним методы), но это не будет рассматриваться в этой статье.

Будучи на стороне клиента, вас волнует, тому ли человеку (серверу) вы посылаете какие-либо личные данные, которые хотите зашифровать. Поэтому сервер, а не клиент, аутентифицируется. Вас также беспокоит участие третей стороны в доступе к вашим данным в том виде, каком вы их посылаете. SSL предоставляет оба этих вида безопасности.

Протокол SSL работает следующим образом:

1. Клиент подключается к веб-серверу и дает список доступных кодов.

2. Сервер берет наиболее устойчивый код, который поддерживает и он, и клиент, и посылает сертификат со своим именем и ключ кодирования, подписанный доверенным Удостоверяющим Центром (Certificate Authority, далее — CA), таким как Verisign.

3. Клиент проверяет сертификат с помощью CA. На практике, хранят набор CA локально, поэтому это может быть сделано без контакта в реальном времени с CA, и поэтому более быстро.

4. Клиент посылает назад случайное число, зашифрованное с помощью публичного ключа сервера. Только клиент знает это число, и только сервер может его расшифровать (используя личный ключ); вот где реализуется безопасность от участия третей стороны.

5. Сервер и клиент используют случайное число для генерирования содержимого ключа, который используется на протяжении передачи данных.

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

Настройка Apache для работы с SSL проста, но есть несколько необходимых шагов. Эта статья рассказывает, как получить сертификат, подписанный CA и как скомпилировать и настроить Apache для работы с SSL. Я использую Apache 2 с mod_ssl. ApacheSSL (реализация Apache с возможностями SSL) также доступен, но сейчас он уже устарел; mod_ssl намного лучше.

Создание сертификата.

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

Теоретически, преимуществом использования пароля являтся повышение защиты. Хотя, на практике пароли не дает такой большой защиты. Если кто-то сможет прочитать или скопировать приватный ключ, значит, у него уже есть доступ к системе и возможность получить пароль, например, используя такую программу, как кейлоггер. Пароль защитит от скрипт-кидди, но не от серьезного взломщика. Возможно, для большинства людей нет смысла его использовать.

Для тестовых целей, или для небольшой локальной сети, вы можете создать сертификат, подписанный вами. Вы можете сделать это, выполнив команду:

openssl req -new -x509 -days 365 -sha1 -newkey rsa:1024 \
 
-nodes -keyout server.key -out server.crt \
 
-subj '/O=Company/OU=Department/CN=www.example.com'

Давайте рассмотрим опции более подробно:

— -x509 означает, что сертификат обязателен, правильнее, чем просто запрос сертификата (смотри ниже)

— -days 365 устанавливает время истечения сертификата, равное году. Вы можете увеличить этот срок. Запомните дату истечения срока, чтобы обновить её при необходимости.

— -sha1 указывает, что будет использован SHA1 для шифрования.

— rsa:1024 делает ключ 1024 битным RSA.

— -nodes указывает отсутствие пароля.

— -keyout и -out указывают, где хранить сертификат и ключ. Ключ должен быть доступен для чтения только для root; сертификат может быть доступен для чтения для world, но должен быть доступным для чтения пользователю, который запускает Apache.

— -subj устанавливает имя компании, имя отделения компании и адрес веб-сайта. Если вы это пропустите, вас попросят это сделать. CN должен совпадать с адресом сайта, иначе сертификат не будет соответствовать, и пользователи будут получать предупреждение при подключении. Убедитесь, что вы не запрашиваете пароль.

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

Не смотря на это, безусловно, лучше использовать сертификат, подписанный доверенным Удостоверяющим Центром, таким как Verisign (который имеет наибольший охват рынка), или меньшей организацией. Большинство браузеров уже имеют набор предустановленных сертификатов, заверенных CA, которые верифицируют сертификат вашего веб-сервера при подключении клиента. Это уменьшает количество трудностей для конечного пользователя и удостоверяет законность вашего сайта.

Чтобы получить сертификат, подписанный CA, прежде всего вы должны создать криптографическую пару и запрос сертификата:

openssl req -new -sha1 -newkey rsa:1024 -nodes \ 
 
-keyout server.key -out www.example.com.csr \ 
 
-subj '/O=Company/OU=Department/CN=www.example.com'

Этот пример работает так же, как и предыдущий, но на этот раз мы не используем опцию -x509. Выполнение этой команды приведет к генерации ключа и запроса сертификата, но не самого сертификата. Если вы ввели CN и так далее, вы не должны вводить адрес e-mail или пароль.

Ключ сервера (файл server.key, который, опять же, должен быть доступен для чтения только для root) остается на вашем веб-сервере; запрос (файл www.example.com.csr) отправляется в CA. Вы можете назвать файл запроса так, как вам вздумается, но назвав его по своему доменному имени, вы упростите задачу для CA.

Следующей стадией будет послать этот файл www.example.com.csr в CA, с вашей оплатой. Они должны вернуть его достаточно быстро, если вы предоставили всю требуемую информацию в вашем запросе. Выбранный вами CA объяснит их действия на своем сайте. Вам может понадобиться поменять формат файла на PEM, но, в случае Verisign, этого делать не придется.

Когда вы получите файл назад в PEM формате, переименуйте его в server.crt (это не является строгой необходимостью, но соответствует условиям Apache) и проверьте его:

    openssl verify -CAfile /path/to/trusted_ca.crt \
 
		-purpose sslserver server.crt

Затем проверьте, что результат выполнения этих двух команд одинаков, т.е., что сертификат соответствует приватному ключу:

    openssl x509 -noout -modulus -in server.pem | openssl sha1
 
    openssl rsa -noout -modulus -in server.key | openssl sha1

Теперь установите ваш ключ (сгенерированный выше как server.key) и сертификат (server.crt) в /etc/apache2/ssl или предпочитаемый вами каталог настроек, если он другой. Как указано выше, очень важно убедиться, что server.key доступен для чтения только для root, в то время как сертификат сервера может быть доступен для чтения для world, но принадлежать и быть доступным для записи только для root.

Компиляция Apache с SSL.

Итак, ваш сертификат сгенерирован. Теперь вам надо настроить свой сервер для его использования.

Для подавляющего большинства людей, лучшим способом установить и управлять Apache2 — модули, полученные через менеджер пакетов вашего дистрибутива. Apache2 из Debian идет вместе с модулем SSL, но он не включен по умолчанию. Для его включения вы должны выполнить команду: a2enmod ssl и перезапустить веб-сервер.

Чтобы сделать это, добавьте строчку

    Include /etc/apache2/mod_ssl.conf

в ваш /etc/apache2/apache2.conf (этот файл может также называться httpd.conf). Вы должны исправить её, обозначив соответствующее расположение файла mod_ssl.conf. Затем перезапустите веб-сервер.

Если вы хотите скомпилировать Apache2 из исходных кодов, в зависимости от того, какие установки вы до этого использовали, вы можете уже иметь или не иметь поддержку SSL. Проверьте это командой apache2 -l. Если перекомпиляция понадобится, запустите ./configure со всеми опциями, которые вы использовали до этого, добавив к ним —enable-ssl и —enable-setenvif (последняя нужна для совместимости с капризами Internet Explorer). Затем установите его как обычно, с помощью make;make install и проверьте правильность прав доступа.

Настройка Apache с SSL.

Следующим шагом будет настройка Apache2. Следующие инструкции приведут к запуску сервера как безопасного (порт 443) и как обычного веб-сервера (порт 80). Прежде всего, вам надо настроить сервер на принятие запросов на оба порта. Или отредактируйте /etc/apache2/ports.conf (в Debian, это входит в apache2.conf), или отредактируйте /etc/apache2/apache2.conf, включив строки:

Listen 80
 
Listen 443

Затем отредактируйте /etc/apache2/sites-enabled/yoursite для использования настроек SSL. Разделение настроек обычного и безопасного сервера с помощью VirtualHost — простейший способ из соображений условий эксплуатации. Любые настройки вне секций VirtualHost (например, установка ServerAdmin) будут применяться для обоих (и любых других) VirtualHost. Добавьте следующий текст в файл конфигурации:

# =================================================
 
# SSL/TLS settings
 
# =================================================
 
NameVirtualHost *:443
 
    DocumentRoot "/local/www/ssl_html"
 
    SSLEngine on
 
    SSLOptions +StrictRequire
 
        SSLRequireSSL
 
    SSLProtocol -all +TLSv1 +SSLv3
 
    SSLCipherSuite HIGH:MEDIUM:!aNULL:+SHA1:+MD5:+HIGH:+MEDIUM
 
    SSLRandomSeed startup file:/dev/urandom 1024
 
    SSLRandomSeed connect file:/dev/urandom 1024
 
    SSLSessionCache shm:/usr/local/apache2/logs/ssl_cache_shm
 
    SSLSessionCacheTimeout 600    
 
    SSLCertificateFile /etc/apache2/ssl/server.crt
 
    SSLCertificateKeyFile /etc/apache2/ssl/server.key
 
    SSLVerifyClient none
 
    SSLProxyEngine off
 
        AddType application/x-x509-ca-cert      .crt
 
        AddType application/x-pkcs7-crl         .crl
 
    SetEnvIf User-Agent ".*MSIE.*" \  
 
      nokeepalive ssl-unclean-shutdown \  
 
      downgrade-1.0 force-response-1.0

Несколько замечаний об этой конфигурации:

— SSLEngine должен быть включен, обозначая, что сервер использует SSL.

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

— SSLRequireSSL запрашивает использование SSL (на этом виртуальном сервере), то есть, пользователь не может подключиться к этому виртуальному хосту с помощью обычного HTTP-запроса. Вот зачем мы разделили безопасное и обычное содержимое.

— SSLProtocol отключает все протоколы, отличные от TLS v1.0 и SSL v3.0. Для современных браузеров все будет работать хорошо.

— SSLCipherSuite устанавливает использование только HIGH и MEDIUM шифров. SHA1 считается более безопасным, чем MD5, поэтому выбран он.

— SSLCertificateFile и SSLCertificateKeyFile указывают расположение файлов сертификата и ключа.

— SSLVerifyClient должна быть установлена как ‘none’, если не используется аутентификация примера.

Чтобы запускать обычный сервер на 80 порту, добавьте следующий текст в конфигурационный файл:

NameVirtualHost: *.80
 
    DocumentRoot "/local/www/html"

После сохранения отредактированного конфигурационного файла, перезапустите сервер. Если вы использовали пароль при генерации сертификата, вам понадобится ввести его при запросе.

Тестирование.

Создайте базовую страницу index.html в корневой директории вашего сервера, если у вас ещё нет содержимого там.

Затем направьте свой браузер на https://yoursite.com. Вы должны увидеть открытие SSL-соединения и загрузку страницы. Если вы используете сертификат, подписанный вами, ваш браузер даст предупреждение о том, что подлинность сервера не может быть проверена. Вы можете выбрать просмотр сертификата и принять его. Если вы используете внешний сертификат, все должно пройти без проблем.

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

Устранение проблем.

Если это работает не так, как ожидалось, прежде всего, проверьте, что ваш сервер вообще запущен с помощью команды ps -a | grep apache. Если она не вернет результата, попробуйте перезапустить сервер и проверьте сообщения об ошибке в консоли.

Также проверьте, что права доступа к файлам сертификата и ключа установлены правильно (см. выше), также, как и права к тестовому HTML-файлу и директории, в которой он находится.

Затем, проверьте логи. Вы должны проверить как логи сервера, так и логи SSL, которые вы настроили в конфигурационном файле выше. Если вы не нашли там ничего полезного, попробуйте поменять значение LogLevel в файле конфигурации Apache2 на ‘debug’, перезапустите Apache2 и протестируйте снова. Это должно дать больше данных в логах.

Если вы запускаете веб-сервер на 80 порту, попробуйте запросить страницу с помощью http://, вместо https://, чтобы понять, заключается ли проблема в веб-сервере или в SSL-соединении. Учтите, что в приведенных выше установках, корневые каталоги веб-сервера разные для http:// и https://, так что вы не можете (или не должны!) получить доступ к тому же содержимому. Если ваша тестовая страница в корневом каталоге http:// работает, в то время, как тестовая страница в корневом каталоге https:// не работает, это поможет вам более точно указать на проблему.

Если проблема в SSL соединении, удобным инструментом будет s_client, который является диагностической утилитой для решения проблем в TLS/SSL-соединениях. Обычное его использование: /usr/bin/openssl s_client -connect localhost:443. Также существует множество других опций, которые вы можете узнать из документации. Если вы получили сообщения об ошибках, это должно вам помочь в определении проблемы.

Заключение.

Поздравляю! Теперь у вас есть рабочий безопасный сервер, с сертификатом, автоматически проверяемым большинством современных браузеров.

Изучаем возможности директории /proc

Перевод статьи Federico Kereki «Discover the possibilities of the /proc directory».

Директория /proc — странный зверь. Она не существует на самом деле, но вы можете заглянуть в неё. Её файлы нулевой длины не являются ни бинарными, ни текстовыми, но вы можете открыть и просмотреть их. Эта специальная директория хранит все детали о вашей системе Linux, включая её ядро, процессы и параметры конфигурации. Изучая директорию /proc, вы можете изучить, как работают команды Linux, и даже сможете делать некоторые задачи администрирования.

В Linux все представлено в виде файла; даже к устройствам доступ происходит, как к файлам (в директории /dev). Не смотря на то, что вы можете подумать, что «нормальные» файлы либо текстовые, либо бинарные (или, возможно, файлы устройств или конвейеров), директория /proc содержит файлы странного типа, — виртуальные файлы. Их можно вывести списком, но на самом деле, они не существуют на диске; операционная система создает их «на лету», если вы делаете попытку их прочитать.

Большинство виртуальных файлов всегда имеют текущую метку даты/времени, говорящую о том, что они постоянно поддерживаются в рабочем состоянии. Директория /proc создается сама по себе каждый раз при загрузке системы. Вам нужно работать с правами суперпользователя, чтобы просмотреть всю директорию; некоторые из файлов (например, связанные с процессами) принадлежат пользователю, запустившему их. Хотя большинство файлов доступны только для чтения, несколько доступных для записи (в особенности в /proc/sys) позволяют вам менять параметры ядра (конечно, вы должны быть осторожны, делая это).

Организация директории /proc

Директория /proc состоит из виртуальных директорий и поддиректорий, которые группируют файлы по определенному принципу. Работая под суперпользователем, команда ls /proc выдаст что-то вроде этого:

1     2432  3340  3715  3762  5441  815
 
129   2474  3358  3716  3764  5445
 
1290  248   3413  3717  3812  5459
 
133   2486  3435  3718  3813  5479
 
1420  2489  3439  3728  3814  557
 
165   276   3450  3731  39    5842
 
166   280   36    3733  3973  5854
 
2     2812  3602  3734  4     6
 
2267  3     3603  3735  40    6381
 
2268  326   3614  3737  4083  6558
 
2282  327   3696  3739  4868  6561
 
2285  3284  3697  3742  4873  6961
 
2295  329   3700  3744  4878  7206
 
2335  3295  3701  3745  5     7207
 
2400  330   3706  3747  5109  7222
 
2401  3318  3709  3749  5112  7225
 
2427  3329  3710  3751  541   7244
 
2428  3336  3714  3753  5440  752
 
devices      modules
 
acpi       diskstats    mounts
 
asound     dma          mtrr
 
bus        execdomains  partitions
 
dri        fb           self
 
driver     filesystems  slabinfo
 
fs         interrupts   splash
 
ide        iomem        stat
 
irq        ioports      swaps
 
net        kallsyms     sysrq-trigger
 
scsi       kcore        timer_list
 
sys        keys         timer_stats
 
sysvipc    key-users    uptime
 
tty        kmsg         version
 
buddyinfo  loadavg      vmcore
 
cmdline    locks        vmstat
 
config.gz  meminfo      zoneinfo
 
cpuinfo    misc

Пронумерованные директории (остановимся на них позже) соответствуют каждому запущенному процессу; специальная символьная ссылка self указывает на текущий процесс. Некоторые виртуальные файлы предоставляют информацию о аппаратном обеспечении, например, /proc/cpuinfo, /proc/meminfo и /proc/interrupts. Другие дают информацию, связанную с файлами, например, /proc/filesystems или /proc/partitions. Файлы в /proc/sys относятся к конфигурации параметров ядра, как мы увидим позже.

Команда cat /proc/meminfo может дать следующее:

# cat /proc/meminfo
 
MemTotal:       483488 kB
 
MemFree:          9348 kB
 
Buffers:          6796 kB
 
Cached:         168292 kB
 
...несколько строк пропущены...

Если вы попробуете команды top или free, вы можете узнать некоторые из этих чисел. Фактически, несколько хорошо известных утилит получают доступ к директории /proc, чтобы получить информацию. Например, если вы хотите узнать, какую версию ядра вы используете, вы можете попробовать uname -srv или спуститься к истокам и набрать cat /proc/version. Есть ещё несколько других интересных файлов:

/proc/apm: содержит информацию о Advanced Power Management, если она установлена.

/proc/acpi: похожая директория, дающая хорошую информацию про более современный ACPI. К примеру, чтобы узнать, подключен ли ваш ноутбук к питанию AC, вы можете выполнить cat /proc/acpi/ac_adapter/AC/state и получить либо «on line», либо «off line»

/proc/cmdline: Показывает параметры, переданные ядру при загрузке. В моем случае, это root=/dev/disk/by-id/scsi-SATA_FUJITSU_MHS2040_NLA5T3314DW3-part3 vga=0x317 resume=/dev/sda2 splash=silent PROFILE=QuintaWiFi, что говорит о том, какой раздел файловой системы является основным, какой режим VGA используется и так далее. Последний параметр сделан с помощью Системы управления настройкой провилей openSUSE.

/proc/cpuinfo: Дает данные о процессоре вашего компьютера. Например, на моем ноутбуке, команда cat /proc/cpuinfo выводит листинг, начинающийся с:

processor       : 0
 
vendor_id       : AuthenticAMD
 
cpu family      : 6
 
model           : 8
 
model name      : Mobile AMD Athlon(tm) XP 2200+
 
stepping        : 1
 
cpu MHz         : 927.549
 
cache size      : 256 KB

Демонстрирующий, что у меня есть только один процессор, под номером 0, семейства 80686 (6 в cpu family идет в качестве средней цифры), — AMD Athlon XP, работающий минимум на 1 Гц.

/proc/loadavg: Файл, показывающий среднюю загрузку процессора; его информация включает использование ЦПУ в последнюю минуту, последние пять минут и последние 10 минут, а также число процессов, работающих в данный момент.

/proc/stat: Также предоставляет статистику, но последней загрузки.

/proc/uptime: Короткий файл, в котором только два числа — сколько секунд ваша система работает и сколько секунд она простаивает.

/proc/devices: Показывает все настроенные и загруженные символьные и блочные устройства. /proc/ide и /proc/scsi дает данные об устройствах IDE и SCSI.

/proc/ioports: Показывает информацию о зонах, используемых для ввода-вывода с вышеуказанными устройствами.

/proc/dma: Показывает используемые каналы DMA.

/proc/filesystems: Показывает типы файловых систем, поддерживаемых вашим ядром. Вот как это может выглядеть:

nodev   sysfs
 
nodev   rootfs
 
nodev   bdev
 
nodev   proc
 
nodev   cpuset
 
...несколько строк пропущено...
 
nodev   ramfs
 
nodev   hugetlbfs
 
nodev   mqueue
 
       ext3
 
nodev   usbfs
 
       ext2
 
nodev   autofs

Первая колонка показывает, какая файловая система монтирована на блочное устройство. В моем случае, у меня есть разделы, настроенные с смонтированными ext2 и ext3.

/proc/mounts: Показывает всё смонтированное, что используется вашим компьютером (его вывод очень похож на /etc/mtab). Проще говоря, /proc/partitions и /proc/swaps покажет все разделы и пространство swap.

/proc/fs: Если вы открываете общий доступ к файловым системам с помощью NFS, в этой директории среди множества поддиректорий и файлов, есть /proc/fs/nfsd/exports, который показывает файловую систему, открытую для общего доступа и права на неё.

/proc/net: Содержит сетевую информацию. Описание каждого из файлов в этой директории займет слишком много места, но в ней есть /dev (каждое сетевое устройство), несколько файлов, связанных с iptables, сетевая статистика и статистика портов, информация о беспроводных подключениях и так далее.

Есть также несколько файлов, связанных с ОЗУ. Я уже упомянул о /proc/meminfo, но у вас также есть /proc/iomem, который показывает, сколько памяти использует ваша система и /proc/kcore, демонстрирующий физическую оперативную память ваше системы. В отличие от большинства других виртуальных файлов, размер /proc/kcore соответствует размеру вашей оперативной памяти и даже немного больше (не пытайтесь выполнить cat /proc/kcore, так как он имеет бинарное содержимое и завалит ваш экран). Наконец, здесь же есть много файлов и директорий, связанных с аппаратным обеспечением, такие как /proc/interrupts и /proc/irq, /proc/pci (все устройства PCI), /proc/bus и так далее, но они включают в себя очень специфическую информацию, которая не нужна большинству пользователей.

Что с процессами?

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

attr             cpuset   fdinfo
 
auxv             cwd      loginuid
 
clear_refs       environ  maps
 
cmdline          exe      mem
 
coredump_filter  fd       mounts
 
mountstats  stat
 
oom_adj     statm
 
oom_score   status
 
root        task
 
smaps       wchan

Давайте изучим основные файлы:

cmdline: Содержит команду, запустившую процесс, со всеми её параметрами.

cwd: Символьная ссылка на текущую рабочую директорию (CWD) процесса; exe ссылается на исполняемый процесс, а root на его корневую директорию.

environ: Показывает все переменные окружения процесса.

fd: Содержит все файловые дескрипторы для процесса, показывающие, какие файлы или устройства он использует.

maps, statm и mem: Работают с памятью, используемой процессом.

stat и status: Предоставляют информацию о статусе процесса, но последний — более четкую и упорядоченную.

Эти файлы дают несколько возможностей для написания скриптов. Например, если вы хотите отследить процессы-зомби, вы можете пройти по всем пронумерованным директориям и проверить содержание строки «(Z) Zombie» в файле /status. Однажды мне надо было проверить, запущена ли определенная программа. Я сделал проход по директориям и проверил файлы /cmdline, в поисках необходимой строки (вы также можете сделать это с помощью команды ps, но в этой статье это не рассматривается). А если вы хотите написать top, который будет выглядеть лучше, вся необходимая информация в ваших руках.

Настройка системы: /proc/sys

/proc/sys не только дает информацию о системе, она также позволяет изменять параметры ядра «на лету», активируя и дезактивируя его возможности (конечно, это может принести и вред вашей системе, — будьте осторожны!).

Чтобы определить, какой файл для настройки, а какой только для чтения, выполните ls -ld. Если файл имеет аттрибут «W», это значит, что вы можете использовать его каким-либо образом для настройки ядра. К примеру, ls -ld /proc/kernel/* начинается примерно так:

dr-xr-xr-x 0 root root 0 2008-01-26 00:49 pty
 
dr-xr-xr-x 0 root root 0 2008-01-26 00:49 random
 
-rw-r--r-- 1 root root 0 2008-01-26 00:49 acct
 
-rw-r--r-- 1 root root 0 2008-01-26 00:49 acpi_video_flags
 
-rw-r--r-- 1 root root 0 2008-01-26 00:49 audit_argv_kb
 
-r--r--r-- 1 root root 0 2008-01-26 00:49 bootloader_type
 
-rw------- 1 root root 0 2008-01-26 00:49 cad_pid
 
-rw------- 1 root root 0 2008-01-26 00:49 cap-bound

Как видете, bootloader_type не может быть изменен, но остальные файлы могут. Чтобы изменить файл, используйте что-то вроде echo 10 >/proc/sys/vm/swappiness. Конкретно этот пример позволит вам настроить производительность страничной организации виртуальной памяти. Кстати, эти изменения — временные и их результаты будут отменены при перезагрузке. Используйте sysctl и файл /etc/sysctl.conf, чтобы сделать более постоянные изменения.

Давайте поверхностно посмотрим на директории в /proc/sys:

debug: Содержит (сюрприз!) отладочную информацию. Это полезно, если вы занимаетесь разработкой ядра.

dev: Предоставляет информацию о специфических устройствах в вашей системе. Например, проверьте директорию /dev/cdrom.

fs: Дает данные о каждом возможном аспекте файловой системы.

kernel: Позволяет вам затрагивать работу и настройки ядра напрямую.

net: Допускает вас до вопросов, связанных с сетью. Будьте осторожны, потому как запутавшись в этом, вы можете потерять подключение.

vm: Работает с подсистемой VM.

Заключение

Специальная директория /proc предоставляет детальную информацию о внутренней работе Linux и позволяет вам улучшить многие аспекты его настройки. Если вы потратите немного больше времени, изучая все возможности этой директории, вы сможете получить более совершенную систему Linux. А не этого ли мы все желаем?

Введение в написание модулей ядра Linux

С давних пор, первым шагом в изучении нового языка программирования, является написание программы, которая печатает «Hello, world!». В этой статье мы используем такой подход при изучении написания простых модулей ядра Linux и драйверов устройств. Мы изучим, как напечатать «Hello, world!» из модуля ядра тремя различными способами: printk(), из /proc файла и из устройства в /dev.

Приготовления.

Модуль ядра — это часть кода ядра, которая может быть динамически загружена и выгружена во время работы ядра. Так как он запускается как часть ядра и ему необходимо близко взаимодействовать с ядром, модуль ядра не может быть скомпилирован в вакууме. Ему нужны, по крайней мере, заголовочные файлы и настройки для ядра, в которое он будет загружен. Компилирование модуля также требует набор средств разработки, такие как компилятор. Для простоты, мы сжато опишем как установить требуемые вещи для создания модуля ядра для Debian, Fedora и «ванильного» ядра Linux из тарболла. Во всех случаях, вы должны скомпилировать свой модуль относительно исходного кода работающего на вашей системе ядра.

Исходные коды ядра обычно находятся в директории /usr/src/linux, права доступа к которой имеет только root. В настоящее время, рекомендуется размещать исходные коды ядра в домашней директории, права на которую будут иметь не-root пользователи. Комманды в этой стстье запускаются не из-под root, используя sudo для временного получения привелегий root-а только тогда, когда это необходимо. Для установки sudo, прочитайте sudo(8), visudo(8) и sudoers(5). Вы можете запускать все комманды и из-под root, если пожелаете. В этом случае, вам потребуются права root-а для того, чтобы следовать инструкциям из этой статьи.

Приготовления для компилирования модулей ядра в Debian.

Пакет module-assistant для Debian настраивает систему для сборки внешних модулей ядра. Установите его с помощью:

$ sudo apt-get install module-assistant

Это все. Теперь вы можете скомпилировать модуль ядра. Дополнительно, вы можете прочитать Debian Linux Kernel Handbook, чтобы иметь более глубокое представление о работе с ядром в Debian.

Приготовления для компилирования модулей в Fedora

Пакет kernel-devel в Fedora имеет все необходимые заголовочные файлы ядра и инструменты для сборки внешних модулей ядра. Установите его с помощью:

$ sudo yum install kernel-devel

И снова, это все, что нужно, — теперь вы можете компилировать модули ядра. Документацию по теме можно найти в Fedora release notes.

«Ванильное» ядро и его настройки.

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

Исходные коды стандартного ядра Linux размещаются на http://kernel.org/. Ссылка на последний стабильный релиз находится на главной странице. Скачайте полностью весь релиз, а не патч. Например, на момент написания статьи, последний стабильный релиз был расположен на http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.21.5.tar.bz2. Для более быстрой загрузки, найдите ближайшее к вам зеркало из списка зеркал и скачивайте с него. Используйте wget, это самый простой способ скачать исходные коды в режиме докачки. HTTP часто блокируется и если скачивание прервется, оно продолжится с того момента, на котором остановилась:

$ wget -c "http://kernel.org/pub/linux/kernel/v2.6/linux-.tar.bz2"

Распакуйте исходные коды ядра:

$ tar xjvf linux-.tar.bz2

Теперь ваше ядро расположено в linux-/. Войдите в эту директорию с начните конфигурацию:

$ cd linux-
 
$ make menuconfig

Существует набор параметров для make, чтобы автоматически собрать и установить ядро в различной форме: пакет Debian, пакет RPM, тарболл и т.д. Чтобы узнать их всех, наберите:

$ make help

Параметр, который будет работать почти на каждом дистрибутиве:

$ make tar-pkg

Когда сборка завершится, установите новое ядро с помощью:

$ sudo tar -C / -xvf linux-.tar

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

$ sudo ln -s 
 
<расположение директории с исходным кодом> 
 
/lib/modules/'uname -r'/build

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

«Hello, World!» с помощью printk()

Для нашего первого модуля, чтобы напечатать «Hello, world!», мы будем использовать модуль, который использует возможность ядра показа сообщений. В общем-то, printk() является printf() для ядра. Сообщения printk() направляются в буффер сообщений ядра и копируются /var/log/messages.

Скачайте тарболл модуля hello_prink и распакуйте его:

$ tar xzvf hello_printk.tar.gz

Он содержит два файла: Makefile, который содержит инструкции для сборки модуля, и hello_printk.c, исходный код модуля. Для начала, осмотрим Makefile:

obj-m := hello_printk.o

obj-m — это список модулей ядра для сборки. .o будет автоматически собран из соответствующего .c файла (нет необходимости указывать список исходных файлов).

KDIR  := /lib/modules/$(shell uname -r)/build

KDIR — это расположение исходного кода ядра. Текущий стандарт это ссылка на соответствующее дерево исходных кодов, содержащее скомпилированные модули.

PWD := $(shell pwd)

PWD — это текущая директория и расположения наших исходных кодов модуля.

default:
 
    $(MAKE) -C $(KDIR) M=$(PWD) modules

default это стандартный параметр make. До тех пор, пока не будет указан другой параметр, make будет исполнять правила для этого параметра. Правило, указанное здесь запускает make с рабочей директорией, содержащей исходные коды ядра и компилирует модули только в $(PWD) (локальной) директории. Это позволяет нам использовать все правила для компилирования модулей, определенные в главном дереве исходного кода ядра.

А сейчас, давайте пройдемся по коду hello_printk.c.

#include
 
#include

Эти включения заголовочных файлов, предоставляемых ядром, требуются для всех модулей. Они включают такие вещи, как определение макроса module_init(), который мы увидим позже.

static int __init
 
hello_init(void)
 
{
 
        printk("Hello, world!\n");
 
        return 0;
 
}

Это функция инициализации модуля, которая выполняется при его первой загрузке. Ключевое слово __init указывает компилятору, что этот код будет выполнен единожды, когда модуль загрузится. Строка, содержащая printk() запишет строку «Hello, world!» в буффер сообщений ядра. Формат аргументов printk(), в большинстве случаев, идентичен формату printf(3).

module_init(hello_init);

Макрос module_init() сообщает ядру, какую функцию выполнить при загрузке модуля. Все остальное, происходящее внутри ядра — результат установок функции инициализации модуля.

static void __exit
 
hello_exit(void)
 
{
 
        printk("Goodbye, world!\n");
 
}
 
module_exit(hello_exit);

Проще говоря, функция выхода запускается один раз, когда модуль выгружается, а макрос module_exit() идентифицирует функцию выхода. Ключевое слово __exit указывает ядру, что этот код нужно выполнить единожды, во время выгрузки модуля.

MODULE_LICENSE("GPL");
 
MODULE_AUTHOR("Valerie Henson ");
 
MODULE_DESCRIPTION("\"Hello, world!\" minimal module");
 
MODULE_VERSION("printk");

MODULE_LICENSE() сообщает ядру, под какой лицензией распространяется исходный код модуля, что влияет на то, к каким символам (функциям, переменным и т.д.) он может получить доступ в главном ядре. Модуль под лицензией GPLv2 (как на примере) имеет доступ ко всем символам. Другие лицензии модуля предупредят ядро о том, что был загружен закрытый модуль, или модуль, которому нельзя доверять. Модули без MODULE_LICENSE() распознаются ядром, как модули, выпущенные не под GPLv2. Макросы MODULE_*() полезны для идентификации информации о модуле в стандартной форме.

Теперь скомпилируем и запустим код. Войдите в директорию с кодом и соберите модуль:

$ cd hello_printk
 
$ make

Затем, загрузите модуль с помощью insmod и проверьте, что он печатает свое сообщение, используя dmesg, программу, которая выводит на экран содержимое буффера сообщений ядра:

$ sudo insmod ./hello_printk.ko
 
$ dmesg | tail

Вы должны увидеть «Hello, world!». А теперь выгрузим модуль, используя rmmod, и проверим сообщение о выходе:

$ sudo rmmod hello_printk
 
$ dmesg | tail

Вы успешно скомпилировали и установили модуль ядра!

«Hello, World!» с помощью /proc

Один из простейших и наиболее популярных способов сделать связь между ядром и пользовательскими программами — файл в /proc. /proc это псевдофайловая система, где операция чтения из файлов возвращает данные, синтезированные ядром, а данные, записываемые в файлы, читаются и оперируются ядром. До появления /proc, связь пользователь-ядро осуществлялась посредством системных вызовов. Использование системных вызовов подразумевает выбор между нахождением системного вызова, который осуществляет нужное действие (зачастую это невозможно), созданием нового системного вызова (требует больших изменений в ядре, использования номера системного вызова, и в общем-то не одобряется), или использованием системного вызова ioctl(), который требует создания специального файла, с которым ioctl() оперирует (может возникнуть ошибка, и ещё больше не одобряется). /proc предоставляет простой, предопределенный путь передачи данных между ядром и пользовательским пространством с полезными инструментами и достаточной свободой действий модулей ядра.

Для наших целей, мы создадим файл в /proc, который выведет «Hello, world!» при чтении. Мы будем использовать /proc/hello_world. Скачайте и распакуйте тарболл hello_proc модуля. Заглянем в код hello_proc.c:

#include
 
#include
 
#include

На этот раз, мы добавили заголовочный файл для procfs, который включает поддержку работы с /proc.

Следующая функция вызовется, когда процесс выполнит read() на созданный нами файл в /proc. Это проще, чем реализовывать это полностью на системном вызове read(), так как нам надо разрешить считать строку «Hello, world!» только один раз.

static int
 
hello_read_proc(char *buffer, char **start, off_t offset, 
 
		int size, int *eof,
 
                void *data)
 
{

Аргументы этой функции нуждаются в подробном объяснении. buffer это указатель на буффер ядра, в который мы пишем выходные данные из read(). start используется для более сложных /proc файлов; здесь мы его игнорируем. offset указывает на то место, с которого надо начинать считывание внутри «файла»; здесь мы передадим 0 для простоты. size это размер буффера в байтах; мы должны удостовериться в том, что мы не пишем данные после того, как буффер закончится. eof указывает на конец файла. Аргумент data, опять же, для более сложных файлов и здесь игнорируется.

А теперь, тело функции:

        char *hello_str = "Hello, world!\n";
 
        int len = strlen(hello_str);
 
        if (size &lt; len)
 
                return&lt; -EINVAL;
 
        if (offset != 0)
 
                return 0;
 
        strcpy(buffer, hello_str);
 
        /*
 
         * Signal EOF.
 
         */
 
        *eof = 1;
 
        return len;
 
}

Затем, нам надо зарегистрировать подсистему /proc в функции инициализации нашего модуля:

static int __init
 
hello_init(void)
 
{
 
        if (create_proc_read_entry("hello_world", 0, 
 
				NULL, hello_read_proc,
 
                                    NULL) == 0) {
 
                printk(KERN_ERR
 
     "Unable to register \"Hello, world!\" proc file\n");
 
                return -ENOMEM;
 
        }
 
        return 0;
 
}
 
module_init(hello_init);

И отменить эту регистрацию, когда модуль выгружен (если мы этого не сделаем, то когда процесс попробует считать /proc/hello_world, файловая система /proc попробует выполнить функцию, которая больше не существует, что вызовет kernel panic).

static void __exit
 
hello_exit(void)
 
{
 
        remove_proc_entry("hello_world", NULL);
 
}
 
module_exit(hello_exit);
 
MODULE_LICENSE("GPL");
 
MODULE_AUTHOR("Valerie Henson ");
 
MODULE_DESCRIPTION("\"Hello, world!\" minimal module");
 
MODULE_VERSION("proc");

Теперь мы готовы скомпилировать и загрузить модуль:

$ cd hello_proc
 
$ make
 
$ sudo insmod ./hello_proc.ko

Появится файл под именем /proc/hello_world, который будет выводить «Hello, world!» при чтении:

$ cat /proc/hello_world
 
Hello, world!

Вы можете создать много других /proc файлов для одного и того же драйвера, добавить функции, позваляющие запись в /proc файлы, создавать директории, полные /proc файлов, и так далее. Для чего-то более сложного, чем этот драйвер, проще и безопаснее использовать вспомогательные функции seq_file при написании интерфейсных функции для /proc. Для расширения своих знаний в этой области, прочтите Driver porting: The seq_file interface.

«Hello, World!» с помощью /dev/hello_world

Теперь мы реализуем «Hello, world!» с помощью файла устройства в /dev, /dev/hello_world. Раньше файл устройства был специальным файлом, создаваемым запуском хитроумного шелл-скрипта под названием MAKEDEV, который вызывал комманду mknod для создания любого возможного файла в /dev, невзирая на то, запущено ли это устройство в системе. Потом был devfs, который создавал /dev файлы, когда к ним впервые обращались, что вызывало много интересных проблем блокировки и множество попыток открыть файл устройства, чтобы проверить его существование. Сейчас с /dev работает udev, который связывает /dev с программами пользовательского окружения. Когда модули ядра регистрируют устройсва, они появляются в файловой системе sysfs, смонтированной в /sys. Программа из пользовательского окружения, udev, следит за изменениями в /sys и динамечески создает записи в /dev, исходя из настроек, обычно размещаемых в /etc/udev.

Скачайте тарболл модуля hello_world. Рассмотрим исходный код hello_dev.c:

#include
 
#include
 
#include
 
#include
 
#include

Как мы видим из списка необходимых заголовочных файлов, для создания устройства требуется немного больше поддержки ядра, чем в предыдущих методах. fs.h включает в себя определения структур для операций над файлами, которые мы должны использовать, применительно к нашему /dev файлу. miscdevice.h включает поддержку регистрации различных файлов устройств. asm/uaccess.h влючает функции для тестирования на чтение или запись в памяти пользовательского окружения, без нарушения прав.

hello_read() — функция, вызываемая процессом системным вызовом read() к /dev/hello. Она выведет «Hello, world!» в буффер, передаваемый вызовом read().

static ssize_t hello_read(struct file * file, char * buf, 
 
                          size_t count, loff_t *ppos)
 
{
 
        char *hello_str = "Hello, world!\n";
 
        int len = strlen(hello_str); 
 
        if (count &lt; len)
 
                return -EINVAL;
 
        if (*ppos != 0)
 
                return 0;
 
        if (copy_to_user(buf, hello_str, len))
 
                return -EINVAL;
 
        *ppos = len;
 
        return len;
 
}

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

static const struct file_operations hello_fops = {
 
        .owner                = THIS_MODULE,
 
        .read                = hello_read,
 
};

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

static struct miscdevice hello_dev = {
 
        MISC_DYNAMIC_MINOR,
 
        /*
 
         * Name ourselves /dev/hello.
 
         */
 
        "hello",
 
        &amp;hello_fops
 
};

Как обычно, мы регистрируем устройство в функции инициализации модуля:

static int __init
 
hello_init(void)
 
{
 
        int ret;
 
        ret = misc_register(&amp;hello_dev);
 
        if (ret)
 
                printk(KERN_ERR
 
          "Unable to register \"Hello, world!\" misc device\n");
 
        return ret;
 
}
 
module_init(hello_init);

И не забываем выгружать устройство в функции выхода:

static void __exit
 
hello_exit(void)
 
{
 
        misc_deregister(&amp;hello_dev);
 
}
 
module_exit(hello_exit);
 
MODULE_LICENSE("GPL");
 
MODULE_AUTHOR("Valerie Henson ");
 
MODULE_DESCRIPTION("\"Hello, world!\" minimal module");
 
MODULE_VERSION("dev");

Скомпилируем и загрузим модуль:

$ cd hello_dev
 
$ make
 
$ sudo insmod ./hello_dev.ko

В системе появилось устройство с именем /dev/hello, которое выводит «Hello, world!» в момент считывания root-ом:

$ sudo cat /dev/hello
 
Hello, world!

Но оно не может считаться обычным пользователем:

$ cat /dev/hello
 
cat: /dev/hello: Permission denied
 
$ ls -l /dev/hello
 
crw-rw---- 1 root root 10, 61 2007-06-20 14:31 /dev/hello

Это происходит при стандартном правиле udev, которое, при появлении нового устройства, создает файл под названием /dev/<имя_устройства> и присваивает ему права на чтение 0660. Нам необходимо создать вместо устройства ссылку, читаемую обычными пользователями, с именем /dev/hello_world. Для того, чтобы это сделать, напишем правило udev.

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

KERNEL=="hello", SYMLINK+="hello_world", MODE="0444"

Разобьем это правило на части и более подробно разберем каждую из них.

KERNEL==»hello» приводит к выполнению правила, когда устройство с именем в строке (== это оператор сравнения) появляется в /sys. Устройство hello появляется когда мы вызываем misc_register() со структура, содержащей имя устройства «hello». Вот результат в /sys:

$ ls -d /sys/class/misc/hello/
 
/sys/class/misc/hello/

SYMLINK+=»hello_world» приказывает добавить hello_world к списку ссылок, которые должны создаваться при появлении устройства. В нашем случае, мы знаем, что список будет состоять из одной ссылки, но другие устройства могут иметь много udev правил, которые создают другие ссылки, так что было бы неплохо это сделать.

MODE=»0444″ назначает права доступа к оригинальному файлу устройства на 0444, что позволяет получить доступ на чтение к этому файлу.

Очень важно использовать правильный оператор (==, += или =), во избежание появления неожиданного результата.

Теперь, когда мы понимаем, что делает это правило, давайте установим его в директорию /etc/udev. Файлы правил udev раположены в той же манере, что и скрипты инициализации System V в /etc/init.d/. Udev выполняет каждый скрипт из директории правил udev, /etc/udev/rules.d, в алфавитном/номерном порядке. Также, как и скрипты инициализации System V, файлы в /etc/udev/rules.d являются обычными ссылками на реально существующие файлы, так что правила будут выполнятся в правильном порядке.

Скопируйте файл hello.rules из директории hello_dev в /etc/udev и создайте ссылку к нему, которая будет выполнятся до других файлов правил:

$ sudo cp hello.rules /etc/udev/
 
$ sudo ln -s ../hello.rules /etc/udev/rules.d/010_hello.rules

Теперь, перезагрузите драйвер hello world и посмотрите на записи в /dev:

$ sudo rmmod hello_dev
 
$ sudo insmod ./hello_dev.ko
 
$ ls -l /dev/hello*
 
cr--r--r-- 1 root root 10, 61 2007-06-19 21:21 /dev/hello
 
lrwxrwxrwx 1 root root      5 2007-06-19 21:21 /dev/hello_world -&gt; hello

Теперь у нас есть /dev/hello_world! Наконец, проверьте, что вы можете считывать устройства «Hello, world!», как обычный пользователь:

$ cat /dev/hello_world
 
Hello, world!
 
$ cat /dev/hello
 
Hello, world!

За более подробной информацией по написанию правил udev, обращайтесь к Writing udev rules, написанную Дэниэлом Дрэйком.

Изучаем параметры 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&lt;10000;i++)
 
        for(j=0;j&lt;5000;j++)
 
                for(k=0;k&lt;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).