Development

Советы для разработчика по работе с MySQL

1. Оптимизируйте запросы для кэша запросов

У большинства MySQL серверов включено кэширование запросов. Один из наилучших способов улучшения производительности — просто предоставить кэширование самой базе данных. Когда какой-либо запрос повторяется много раз, его результат берется из кэша, что гораздо быстрее прямого обращения к базе данных.
Основная проблема в том, что многие просто используют запросы, которые не могут быть закэшированны:

      // запрос не будет кэширован
      $r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");
      // а так будет!
      $today = DATE("Y-m-d");
      $r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");

Причина в том, что в первом запросе используется функция CURDATE(). Это относиться ко всем функциям, подобным NOW(), RAND() и другим, результат которых недетерминирован. Если результат функции может измениться, то MySQL не кэширует такой запрос. В данном примере это можно предотвратить вычислением даты до выполнения запроса.

2. Используйте EXPLAIN для ваших запросов SELECT

Используя EXPLAIN, вы можете посмотреть, как именно MySQL выполняет ваш запрос. Это может помочь вам избавиться от слабых мест производительности и других проблем в вашем запросе или в структуре таблиц.
Результат EXPLAIN покажет вам, какие используются индексы, как выбираются и сортируются таблицы и т.д.
Возьмите ваш SELECT запрос (он может быть сложным, с объединениями) и добавьте в начало ключевое слово EXPLAIN. Для этого вы можете использовать phpmyadmin. В результате вы получите очень интересную таблицу. Для примера, пусть я забыл добавить индекс в таблицу, которая участвует в объединении:

Explain для неоптимизированного запроса

После добавления индекса для поля group_id:
Explain после оптимизации запроса

Теперь вместо 7883 строк, выбираются только 9 и 16 строк из двух таблиц. Перемножение всех чисел в столбце rows даст число прямо пропорциональное производительности запроса.

3. LIMIT 1, когда нужна единственная строка

Иногда, обращаясь к таблице, вы точно знаете, что вам нужна только одна конкретная строка. Например, нужно получить одну уникальную строку или просто проверить существование записей, удовлетворяющих запросу WHERE.
В этом случае, добавление LIMIT 1 в ваш запрос будет оптимальнее. Таким образом, база данных остановит выборку записей, после нахождения первой же, вместо того, чтобы выбрать всю таблицу или индекс.

      // есть пользователи в Alabama?
      // можно так:
      $r = mysql_query("SELECT * FROM user WHERE state = 'Alabama'");
      IF (mysql_num_rows($r) > 0) {
      // ...
      }
      // но так лучше:
      $r = mysql_query("SELECT * FROM user WHERE state = 'Alabama' LIMIT 1");
      IF (mysql_num_rows($r) > 0) {
      // ...
      }

4. Индексируйте поля, по которым ищите

Индекс это не только основной или уникальный ключ. Это так же любые столбцы в таблице, которые вы используете для поиска и их можно проиндексировать.

Результат использования индекса

Как вы можете заметить, это правило также применимо для части строк, например — «last_name LIKE ‘a%’». При поиске с начала строки, MySQL использует индекс этого столбца.
Вы так же должны понимать, что это не сработает для регулярных выражений. Например, когда вы ищите слово (т.е. «WHERE post_content LIKE ‘%apple%’»), то от обычного индекса не будет никакого толку. Лучше будет использовать полнотекстовый поиск или создать вашу собственную систему индексации.

5. Индексируйте поля для объединения и используйте для них одинаковые типы столбцов

Если ваше приложение содержит много объединений таблиц, вам необходимо проиндексировать в обеих таблицах поля, используемые для объединения. Это повлияет на то, как MySQL делает внутреннюю оптимизацию объединений.
Так же эти столбцы должны быть одного типа. Например, если вы объединяете столбец DECIMAL со столбцом INT из другой таблицы, MySQL не сможет использовать хотя бы один из индексов. Даже кодировки символов должны быть одного типа для строковых столбцов.

      // выборки компаний в штате пользователя
      $r = mysql_query("SELECT company_name FROM users
      JOIN companies ON (users.state = companies.state)
      users.id = $user_id");
      // обе колонки state должны быть проиндексированны
      // они обе должны иметь один тип данных и кодировку символов
      // а иначе MySQL сделает полную выборку из этих таблиц

6. Не используйте ORDER BY RAND()

(Имеется в виду выборка единственной строки. Примечание переводчика)

Это одна из тех вещей, который выглядят очень хорошо на первый взгляд, но многие начинающие программисты попались на эту удочку. Вы даже не представляете, какое слабое место в производительности возникнет, если будете использовать это в запросах.
Если вам действительно нужен случайный порядок строк в запросе, то есть лучшие способы сделать это. Конечно, это приведет к дополнительному коду, но позволит избавиться от слабого места в производительности, которое будет сужаться экспоненциально при увеличении данных. Проблема в том, что MySQL будет выполнять RAND() (а это нагрузка на процессор) для каждой строки при сортировке, выдавая только одну строку.

Таким образом вы выберите случайный номер, который меньше количества строк и используете его для смещения в LIMIT.

7. Избегайте SELECT *

Чем больше данных считывается из таблицы, тем медленнее запрос. Это увеличивает время работы с хранилищем данных. Также, когда сервер базы данных установлен отдельно от web-сервера, будет большая задержка при передаче данных по сети.
Прописывать, какие именно столбцы из запроса вам нужны — хорошая привычка.

      // не очень хорошо:
      $r = mysql_query(«SELECT * FROM USER WHERE user_id = 1»);
      $d = mysql_fetch_assoc($r);
      echo «Welcome {$d['username']}»;
 
      // лучше:
      $r = mysql_query(«SELECT username FROM USER WHERE user_id = 1»);
      $d = mysql_fetch_assoc($r);
      echo «Welcome {$d['username']}»;
 
      // разница более значительна при большем наборе данных.

8. Старайтесь всегда создать поле ID

В каждой таблице нужно поле id, которое будет PRIMARY KEY, AUTO_INCREMENT, а так же иметь тип INT. Так же неплохо, чтобы оно было UNSIGNED, т.к. вряд ли у идентификатора будут отрицательные значения.
Даже если в вашей таблице пользователей есть уникальное поле username, не делаете его основным ключом. Использование поля VARCHAR, как основного ключа, очень медлительно. Да и структура вашего кода, относящаяся к пользователям, будет гораздо лучше, если у каждого пользователя будет свой внутренний идентификатор.
Есть так же и внутренние операции MySQL, использующие первичный ключ. И это становиться очень важно для более сложных конфигураций базы данных (кластеры, распараллеливание и т.д.)
Исключение из этого правила составляют «таблицы ассоциаций», используемые для связи «многие-ко-многим» между 2 таблицами. Например, таблица «posts_tags», содержит 2 поля: post_id, tag_id, который используется для объединения между двумя таблицами «Posts» и «Tags». Эта таблица будет иметь первичный ключ составленный из 2 полей.

9. Используйте ENUM вместо VARCHAR

ENUM — очень быстрый и компактный тип поля. Значения в нем храниться так же, как TINYINT, но отображаются как в строковом поле. Это делает его незаменимым в некоторых случаях.
Если у вас есть поле, в котором будет вполне определенный набор значений, используйте ENUM вместо VARCHAR. Например, если есть поле «status», его значения могут быть «active», «inactive», «pending», «expired» и т.д.
Можно даже получить от MySQL «совет» о том, как перестроить таблицу. Если у вас есть поле VARCHAR, MySQL может предложить заменить его на ENUM. Для этого используется PROCEDURE ANALYSE(), описанная ниже.

10. Используйте подсказки от PROCEDURE ANALYSE()

PROCEDURE ANALYSE() анализирует структуру вашей таблицы и данные в ней, и выдает возможные советы по оптимизации. Это возможно только при наличии реальных данных в таблице, т.к. анализ делается в основном на их основе.
Например, если вы создали первичный ключ типа INT, а записей не очень много, MySQL может предложить заменить его на MEDIUMINT. Или, если используется VARCHAR в котором есть несколько уникальных значений, будет предложен ENUM.
В phpmyadmin в структуре таблице есть ссылка «Анализ структуры таблицы», результат которой может быть, например, следующим:

Подсказки в phpmyadmin от работы функции PROCEDURE ANALYSE()

11. Используйте NOT NULL, если это возможно

Если есть особые причины использовать NULL — используйте его. Но перед этим спросите себя — есть ли разница между пустой строкой и NULL (для INT — 0 или NULL). Если таких причин нет, используйте NOT NULL.
NULL занимает больше места и, к тому же, усложняет сравнения с таким полем. Избегайте его, если это возможно. Тем не менее, бывают веские причины использовать NULL, это не всегда плохо.
Из документации MySQL:
«Столбцы NULL занимают больше места в записи, из-за необходимости отмечать, что это NULL значение. Для таблиц MyISAM, каждое поле с NULL занимает 1 дополнительный бит, который округляется до ближайшего байта».

11. Используйте NOT NULL, если это возможно

Если есть особые причины использовать NULL — используйте его. Но перед этим спросите себя — есть ли разница между пустой строкой и NULL (для INT — 0 или NULL). Если таких причин нет, используйте NOT NULL.
NULL занимает больше места и, к тому же, усложняет сравнения с таким полем. Избегайте его, если это возможно. Тем не менее, бывают веские причины использовать NULL, это не всегда плохо.
Из документации MySQL:
«Столбцы NULL занимают больше места в записи, из-за необходимости отмечать, что это NULL значение. Для таблиц MyISAM, каждое поле с NULL занимает 1 дополнительный бит, который округляется до ближайшего байта».

12. Prepared Statements

Есть множество преимуществ в использовании prepared statements, как для безопасности, так и для улучшения производительности. Prepared statements фильтруют значения данных, добавляемых в запрос, что защищает запросы от SQL инъекций. Конечно, вы можете фильтровать переменные вручную, но тут может сказаться человеческая забывчивость и невнимательность. Конечно, это не столь важно при использовании какого-либо фреймворка или ORM.
Поскольку статья посвящена оптимизации, отмечу также выгоды для нее. Они проявляются, когда запрос выполняется много раз в приложении. Вы можете использовать для prepared statement разные значения, но MySQL будет разбирать запрос только один раз.
Кроме того, последние версии MySQL компилируют prepared statements в бинарную форму, что позволяет повысить эффективность.
Раньше многие программисты избегали prepared statements по одной единственной причине — они не кэшировались MySQL, но с версии 5.1 это не так.
Посмотрите mysqli extension для использования prepared statements или воспользуйтесь абстракцией базы данных, например, PDO.

      // создаем a prepared statement
      IF ($stmt = $mysqli->PREPARE("SELECT username FROM user WHERE state=?")) {
      // привязываем значения
      $stmt->bind_param("s", $state);
      // выполняем
      $stmt->EXECUTE();
      // привязываем результат
      $stmt->bind_result($username);
      // получаем данные
      $stmt->fetch();
      printf("%s is from %s\n", $username, $state);
      $stmt->close();
      }

13. Небуферизованные запросы

Обычно, делая запрос, скрипт останавливается и ждет результата его выполнения. Вы можете изменить это, используя небуферизованные запросы.
Хорошее описание есть в документации функции mysql_unbuffered_query():

«mysql_unbuffered_query() отправляет SQL-запрос в MySQL, не извлекая и не автоматически буферизуя результирующие ряды, как это делает mysql_query(). С одной стороны, это сохраняет значительное количество памяти для SQL-запросов, дающих большие результирующие наборы. С другой стороны, вы можете начать работу с результирующим набором срезу после получения первого ряда: вам не нужно ожидать выполнения полного SQL-запроса»

Однако есть определенные ограничения. Вам придется считывать все записи или вызывать mysql_free_result() прежде, чем вы сможете выполнить другой запрос. Так же вы не можете использовать mysql_num_rows() или mysql_data_seek() для результата функции.

14. Храните IP в UNSIGNED INT

Многие программисты хранят IP адреса в поле типа VARCHAR(15), не зная что можно хранить его в целочисленном виде. INT занимает 4 байта и имеет фиксированный размер поля.
Убедитесь, что используете UNSIGNED INT, т.к. IP можно записать как 32 битное беззнаковое число.
Используйте в запросе INET_ATON() для конвертирования IP адреса в число, и INET_NTOA() для обратного преобразования. Такие же, такие функции есть и в PHP — ip2long() и long2ip() (в php эти функции могут вернуть и отрицательные значения. замечание от хабраюзера The_Lion).

      $r = "UPDATE users SET ip = INET_ATON('{$_SERVER['REMOTE_ADDR']}') WHERE user_id = $user_id";

15. Таблицы фиксированного размера (статичные) — быстрее

Если каждая колонка в таблице имеет фиксированный размер, то такая таблица называется «статичной» или «фиксированного размера». Пример колонок не фиксированной длины: VARCHAR, TEXT, BLOB. Если включить в таблицу такое поле, она перестанет быть фиксированной и будет обрабатываться MySQL по-другому.
Использование таких таблицы увеличит эффективность, т.к. MySQL может просматривать записи в них быстрее. Когда надо выбрать нужную строку таблицы, MySQL может очень быстро вычислить ее позицию. Если размер записи не фиксирован, ее поиск происходит по индексу.
Так же эти таблицы проще кэшировать и восстанавливать после падения базы. Например, если перевести VARCHAR(20) в CHAR(20), запись будет занимать 20 байтов, вне зависимости от ее реального содержания.
Используя метод «вертикального разделения», вы можете вынести столбцы с переменной длиной строки в отдельную таблицу.

16. Вертикальное разделение

Вертикальное разделение — означает разделение таблицы по столбцам для увеличения производительности.
Пример 1. Если в таблице пользователей хранятся адреса, то не факт что они будут нужны вам очень часто. Вы можете разбить таблицу и хранить адреса в отдельной таблице. Таким образом, таблица пользователей сократиться в размере. Производительность возрастет.
Пример 2. У вас есть поле «last_login» в таблице. Оно обновляется при каждом входе пользователя на сайт. Но все изменения в таблице очищают ее кэш. Храня это поле в другой таблице, вы сведете изменения в таблице пользователей к минимуму.
Но если вы будете постоянно использовать объединение этих таблиц, это приведет к ухудшению производительности.

17. Разделяйте большие запросы DELETE и INSERT

Если вам необходимо сделать большой запрос на удаление или вставку данных, надо быть осторожным, чтобы не нарушить работу приложения. Выполнение большого запроса может заблокировать таблицу и привести к неправильной работе всего приложения.
Apache может выполнять несколько параллельных процессов одновременно. Поэтому он работает более эффективно, если скрипты выполняются как можно быстрее.
Если вы блокируете таблицы на долгий срок (например, на 30 секунд или дольше), то при большой посещаемости сайта, может возникнуть большая очередь процессов и запросов, что может привести к медленной работе сайта или даже к падению сервера.
Если у вас есть такие запросы, используйте LIMIT, чтобы выполнять их небольшими сериями.

      while (1) {
      mysql_query("DELETE FROM logs WHERE log_date <= '2009-10-01' LIMIT 10000");
      IF (mysql_affected_rows() == 0) {
      // удалили
      break;
      }
      // небольшая пауза
      usleep(50000);
      }

18. Маленькие столбцы быстрее

Для базы данных работа с жестким диском, возможно, является самым слабым местом. Маленькие и компактные записи обычно лучше с точки зрения производительности, т.к. уменьшают работу с диском.
В документации к MySQL есть список требований к хранилищам данных для всех типов данных.
Если ваша таблица будет хранить мало строк, то не имеет смысла делать основной ключ типом INT, возможно лучше будет сделать его MEDIUMINT, SMALLINT или даже TINYINT. Если вам не нужно хранить время, используйте DATE вместо DATETIME.
Однако будьте осторожны, что бы не вышло как с Slashdot.

19. Выбирайте правильный тип таблицы

Два основных типа таблиц — MyISAM и InnoDB, у каждого есть свои плюсы и минусы.
MyISAM хорошо считывает из таблиц большое количество данных, но он плох для записи. Даже если вы изменяете всего одну строку, блокируется вся таблица, и ни один процесс не может ничего из нее прочитать. MyISAM очень быстро выполняет запросы типа SELECT COUNT(*).
У InnoDB более сложный механизм хранения данных, и он может быть медленнее, чем MyISAM, для маленьких приложений. Но он поддерживает блокировку строк, что более эффективно при масштабировании. Так же поддерживаются некоторые дополнительные функции, такие операции как транзакции.
Подробнее:MyISAM Storage EngineInnoDB Storage Engine

20. Используте ORM

Используя ORM, можно получить определенную оптимизацию работы. Все, что можно сделать с помощью ORM, можно сделать и вручную. Но это требует дополнительной работы и более высокого уровня знаний.
ORM замечателен для «ленивой» загрузки данных. Это означает выборку данных по мере необходимости. Но необходимо быть осторожным, т.к это может привести к появлению множества маленьких запросов, что приведет к снижению производительности.
ORM также может объединять несколько запросов в пакеты, вместо отправки каждого отдельно.
Моя любимая ORM для PHP — Doctrine. Я уже писал статью об установке Doctrine в CodeIgniter.

21. Будьте осторожны с постоянными соединениями

Постоянные соединения предназначены для уменьшения расходов на установление связи с MySQL. Когда соединение создается, оно остается открытым после завершения работы скрипта. В следующий раз, этот скрипт воспользуется этим же соединением.mysql_pconnect() в PHPНо это звучит хорошо только в теории. Из моего личного опыта (и опыта других), использование этой возможности не оправдывается. У вас будут серьезные проблемы с ограничением по числу подключений, памятью и так далее.
Apache создает много параллельных потоков. Это основная причина, почему постоянные соединения не работаю так хорошо, как бы хотелось. Перед использованием mysql_pconnect() посоветуйтесь с вашим сисадмином.

Источник

By sysadmin on 16.02.2010 | Development, MySQL
Метки: ,

Освобождаем консоль

Большинство Linux-приложений с графическим пользовательским интерфейсом при запуске из консоли не освобождают консоль, а запирают сесию и начинают выводить в ней свои диагностические сообщения. Нажатие Ctrl+C в этом случае приводит к остановке приложения.Сейчас я покажу как заставить свои приложения освобождать консоль.

Добавляем в функцию main() своего приложения следующий код:

      if (fork() != 0){
          exit(0);
      }
 
      close(0);
      close (1);
      close(2);
 
      int fd = open("/dev/null", O_RDWR);
      dup2(fd, 0);
      dup2(fd, 1);
      dup2(fd, 2);

Первые три строки создают новую копию процесса нашей программы и убивают родительский процесс. Затем с помощью функции close() закрываем все дескрипторы ввода-вывода, унаследованные от родительского процесса, создаем новые дескрипторы, адресованные в /dev/null, и присваиваем их своей программе.

By sysadmin on 26.05.2009 | Development
Метки: , , , , , ,

Коды ответов (ошибок) HTTP сервера

При любом HTTP запросе сервер сначала возвращает код ответа на HTML запрос. Проанализировав этот ответ можно сделать вывод о том, был ли запрос выполнен успешно, или в процессе обработки запроса произошла ошибка.

Коды ответа HTTP сервера могут принадлежать следующим группам:
1xx — Информационный ответ
2xx — Успешная обработка запроса
3xx — Переадресация (редирект)
4xx — Неполный запрос к серверу
5xx — При обработке запроса произошла ошибка

Естественно, что кодов ответа в каждой группе не по 100. Браузер (клиент IE, Opera, Mozilla и т.д.), получая от сервера тот или иной код, сам решает как его интерпретировать. Если код клиенту неизвестен, то как минимум он может определить диапазон кода и повести себя соответствующим образом.

Информационные ответы

100 Continue
Часть запроса принята. Как правило это занчит, что можно отправлять следующую часть запроса.

101 Switching Protocols
Сервер производит переключение протоколов в соответствии с заголовком Upgrade.

Успешная обработка запроса

200 OK
Запрос обработан успешно.

201 Created
Данный код используется когда происходит создание нового URI. Вместе с кодом сервер посылает заголовок Location с адресом нового URI.

202 Accepted
Запрос принят и обрабатывается. В теле ответа как правило содержится дополнительная информация.

203 Non-Authoritative Information
Ответ означает, что информация получена из ненадежного источника (например, с другого сервера).

204 No Content
Запрос обработан, но в ответ ничего не возвращается. Как правило используется если в ответ на запрос не нужно обновлять содержимое документа.

205 Reset Content
Означает, что содержимое документа должно быть сброшено в начальное состояние. Обычно используется при очистке форм ввода данных..

206 Partial Content
При данном ответе возвращается лишь часть данных. Обычно используется если клиент запросил часть данных с использованием заголовка Range.

Переадресация

300 Multiple Choices
Означает, что существует несколько вариантов запрашиваемой страницы. Например, сайт, переведенный на несколько языков.

301 Moved Permanently
Данный ответ означает, что данный документ был перемещен и клиенту следует изменить все ссылки на данный документ его новым местоположением.

302 Moved Temporarily
Документ временно перемещен в другое место.

303 See Other
Данный документ можно найте по другим ссылкам. Список ссылок передан в теле ответа.

304 Not Modified
Данный код ответа возвращается если был запрос lf-Modified-Since, и документ не изменялся с указанной даты.

305 Use Proxy
Доступ к документу должен осуществляться через proxy-сервер, адрес которого указан в Location.

Неполные запросы клиента

400 Bad Request
Ошибка в строке запроса.

401 Unauthorized
Ответ означает, что пользователь не имеет достаточных прав для просмотра документа.

402 Payment Required
Данный код зарезервирован на будущее. Видимо будет означать, что запрошенный документ является платным ресурсом.

403 Forbidden
Запрос не будет выполнен по какой-либо причине.

404 Not Found
Запрашиваемого документа нет на сервере.

405 Method Not Allowed
Означает, что метод, используемый клиентом, не поддерживается.

406 Not Acceptable
Ресурс существует, но не в той форме, что клиент запросил. Например, может различаться язык документа.

407 Proxy Authentication Required
Для Proxy-сервера необходима авторизация.

408 Request Time-out
Сервер разорвал соединение из-за превышенного таймаута.

409 Conflict
Запрос конфликтует с другим запросом.

410 Gone
Данный код означает, что документ был удален с сервера.

411 Length Required
Пропущено необходимое поле в заголовке запроса Content-Length.

412 Precondition Failed
Условие указанное в заголовке не выполняется.

413 Request Entity Too Large
Слишком большое тело запроса.

414 Request-URI Too Long
Слишком длинный URI в запросе.

415 Unsupported Media Type
Сервер не поддерживает указанный формат данных.

Ошибки сервера

500 Internal Server Error
Внутренняя ошибка сервера. Например, ошибка при выполнении скрипта.

501 Not Implemented
Недопустимое действие.

502 Bad Gateway
Недопустимый ответ с другого ресурса.

503 Service Unavailable
Данный код означает, что указанный сервис временно недоступен.

504 Gateway Time-out
Превышен таймаут ожидания от другого ресурса

505 HTTP Version not supported
Данная версия протокола HTTP не поддерживается сервером.

By sysadmin on 28.04.2009 | Development, FreeBSD, Linux, Windows
Метки: , , , ,

Установка модулей Perl вручную и используя CPAN

Установка модулей для Perl требуется для работы различных утилит, применяемх в повседневной рутине администрирования. Установка Perl модулей вручную с удоволетворением всех зависимостей это довольно скучный и нудный процесс.

Установка Perl модулей с испоьльзованием CPAN намного более лучшее решение, так как все зависимости определятся и установятся автоматически. В это статье я опишу оба метода установки модулей.

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

<strong>Can’t locate</strong> XML/parser.pm in @INC (@INC contains:
/usr/lib/perl5/5.10.0/i386-linux-thread-multi
/usr/lib/perl5/5.10.0
/usr/local/lib/perl5/site_perl/5.10.0/i386-linux-thread-multi
/usr/local/lib/perl5/site_perl/5.10.0
/usr/lib/perl5/vendor_perl/5.10.0/i386-linux-thread-multi
/usr/lib/perl5/vendor_perl/5.10.0 /usr/lib/perl5/vendor_perl
/usr/lib/perl5/site_perl/5.10.0 .)

Установка Perl модулей вручную

Download Perl module

Перейдите на веб-сайт CPAN Search и найдите модуль который вам нужно скачать. В этом пример мы скачаем и установим модуль XML:Parser Perl. Я скачаю XML-Parser-2.36.tar.gz в /home/download

$ cd /home/download
$ gzip -d XML-Parser-2.36.tar.gz
$ tar xvf XML-Parser-2.36.tar
$ cd XML-Parser-2.36

Собираем модуль

$ perl Makefile.PL
Checking if your kit is complete…
Looks good
Writing Makefile for XML:Parser::Expat
Writing Makefile for XML:Parser
$ make
$ make test

Устанавливаем модуль

$ make install

Это простой модуль без зависимостей, поэтому он установился без проблем. Обычно, любой модуль Perl имеет несколько зависимостей. Ставить все модули поочередно описанным выше методом скучнейшая задача. Я рекомендую использовать для установки CPAN метод, описанный ниже. Вручную стоит собирать модули в случае отсутсвия подключения к интернету.

Автоматическая установка Perl модулей с использованием CPAN

Проверяем установлен ли CPAN

Для установки Perl модулей используя CPAN, убедитесь что команда cpan работает. В этом примере, модуль CPAN ещё не установлен.

$ cpan
-bash: cpan: command not found
$ perl -MCPAN -e shell
Can’t locate CPAN.pm in @INC (@INC contains:
/usr/lib/perl5/5.10.0/i386-linux-thread-multi
/usr/lib/perl5/5.10.0
/usr/local/lib/perl5/site_perl/5.10.0/i386-linux-thread-multi
/usr/local/lib/perl5/site_perl/5.10.0
/usr/lib/perl5/vendor_perl/5.10.0/i386-linux-thread-multi
/usr/lib/perl5/vendor_perl/5.10.0
/usr/lib/perl5/vendor_perl /usr/lib/perl5/site_perl/5.10.0 .).
BEGIN failed–compilation aborted.

Установка модуля CPAN с помощью yum

$ yum install perl-CPAN

Результат работы команды:

Loaded plugins: refresh-packagekit
updates-newkey                       | 2.3 kB     00:00
primary.sqlite.bz2                   | 2.4 MB     00:00
Setting up Install Process
Parsing package install arguments
Resolving Dependencies
Transaction Summary
=============================================================================
Install      5 Package(s)
Update       0 Package(s)
Remove       0 Package(s)
Total download size: 1.0 M
Is this ok [y/N]: y
Downloading Packages:
(1/5): perl-ExtUtils-ParseXS-2.18-31.fc9.i386.rpm     |  30 kB     00:00
(2/5): perl-Test-Harness-2.64-31.fc9.i386.rpm         |  70 kB     00:00
(3/5): perl-CPAN-1.9205-31.fc9.i386.rpm               | 217 kB     00:00
(4/5): perl-ExtUtils-MakeMaker-6.36-31.fc9.i386.rpm   | 284 kB     00:00
(5/5): perl-devel-5.10.0-31.fc9.i386.rpm              | 408 kB     00:00
 
Installing     : perl-ExtUtils-ParseXS                             [1/5]
Installing     : perl-devel                                        [2/5]
Installing     : perl-Test-Harness                                 [3/5]
Installing     : perl-ExtUtils-MakeMaker                           [4/5]
Installing     : perl-CPAN                                         [5/5]
 
Installed: perl-CPAN.i386 0:1.9205-31.fc9
Dependency Installed:
  perl-ExtUtils-MakeMaker.i386 0:6.36-31.fc9
  perl-ExtUtils-ParseXS.i386 1:2.18-31.fc9
  perl-Test-Harness.i386 0:2.64-31.fc9
  perl-devel.i386 4:5.10.0-31.fc9
Complete!

Настраиваем span

При первом вызове cpan вы должны указать некоторые конфигурационные параметры как показано ниже. Я покажу только важные параметры конфигурации. Значения по умолчанию принимаются нажатием клавиши enter.

$ cpan
Sorry, we have to rerun the configuration dialog for CPAN.pm due
to some missing parameters…
CPAN build and cache directory? [/root/.cpan]
Download target directory? [/root/.cpan/sources]
Directory where the build process takes place? [/root/.cpan/build]
Always commit changes to config variables to disk? [no]
Cache size for build directory (in MB)? [100]
Let the index expire after how many days? [1]
Perform cache scanning (atstart or never)? [atstart]
Cache metadata (yes/no)? [yes]
Policy on building prerequisites (follow, ask or ignore)? [ask]
Parameters for the ‘perl Makefile.PL’ command? []
Parameters for the ‘perl Build.PL’ command? []
Your ftp_proxy? []
Your http_proxy? []
Your no_proxy? []
Is it OK to try to connect to the Internet? [yes]
First, pick a nearby continent and country by typing in the number(s)
(1) Africa
(2) Asia
(3) Central America
(4) Europe
(5) North America
(6) Oceania
(7) South America
Select your continent (or several nearby continents) [] 5
(1) Bahamas
(2) Canada
(3) Mexico
(4) United States
Select your country (or several nearby countries) [] 4
(2) ftp://carroll.cac.psu.edu/pub/CPAN/
(3) ftp://cpan-du.viaverio.com/pub/CPAN/
(4) ftp://cpan-sj.viaverio.com/pub/CPAN/
(5) ftp://cpan.calvin.edu/pub/CPAN
(6) ftp://cpan.cs.utah.edu/pub/CPAN/
e.g. ‘1 4 5′ or ‘7 1-4 8[] 2-16
cpan[1]> o conf commit
commit: wrote ‘/usr/lib/perl5/5.10.0/CPAN/Config.pm’
cpan[2]> quit
No history written (no histfile specified).
Lockfile removed.

Установка Perl модулей с использованием CPAN

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

$ /usr/bin/perl -MCPAN -einstall Email::Reply’
(или)
$ cpan
cpan shell — CPAN exploration and modules installation (v1.9205)
ReadLine support available (maybe install Bundle::CPAN or Bundle::CPANxxl?)
cpan[1]> install “Email::Reply”;

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

CPAN: Storable loaded ok (v2.18)
Going to read /root/.cpan/Metadata
Database was generated on Mon, 15 Sep 2008 11:02:52 GMT
Running install for module 'Email::Reply'
Running make for R/RJ/RJBS/Email-Reply-1.202.tar.gz
CPAN: LWP::UserAgent loaded ok (v2.036)
CPAN: Time::HiRes loaded ok (v1.9711)
Fetching with LWP:
ftp://carroll.cac.psu.edu/pub/CPAN/authors/id/R/RJ/RJBS/Email-Reply-1.202.tar.gz
CPAN: checksum security checks disabled because Digest::SHA not installed.
Please consider installing the Digest::SHA module.
CPAN: Compress::Zlib loaded ok (v2.008)
Email-Reply-1.202/
Email-Reply-1.202/Changes
CPAN: File::Temp loaded ok (v0.18)
Warning: prerequisite Email::Abstract 2.01 not found.
Warning: prerequisite Email::MIME::Creator 1.41 not found.
Writing Makefile for Email::Reply
---- Unsatisfied dependencies detected during ----
----       RJBS/Email-Reply-1.202.tar.gz      ----
Email::Abstract [requires]
Email::MIME::Creator [requires]
Shall I follow them and prepend them to the queue
of modules we are processing right now? [yes]
[Note: CPAN automatically detects that Email::Abstract
       and Email::MIME::Creator is required]
Fetching with LWP:
ftp://server/pub/CPAN/authors/id/R/RJ/RJBS/Email-Abstract-2.134.tar.gz
make — OK
make install  — OK
Fetching with LWP:
ftp://server/pub/CPAN/authors/id/R/RJ/RJBS/Email-MIME-Creator-1.454.tar.gz
make — OK
make install  — OK
Warning: prerequisite Email::Simple::Creator 1.4 not found.
Writing Makefile for Email::MIME::Creator
Email::Simple::Creator [requires]
Shall I follow them and prepend them to the queue
of modules we are processing right now? [yes]
[Note: CPAN automatically detects that Simple::Creator is required]
Fetching with LWP:
ftp://carroll.cac.psu.edu/pub/CPAN/authors/id/R/RJ/RJBS/Simple-Creator.tar.gz
make — OK
make install  — OK
Fetching with LWP:
CPAN.pm: Going to build R/RJ/RJBS/Email-Reply-1.202.tar.gz
make — OK
make install  — OK

В примере выше Email::Reply зависит от нескольких других модулей. CPAN автоматически определил зависимости и установил Email::Reply и все другие необходимые модули.

By sysadmin on 02.03.2009 | Development
Метки: , , , ,

Установка расширений 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 &lt; 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
    By sysadmin on 26.02.2009 | Development
    Метки: , ,

    Умное Кеширование и Версионность в Javascript/CSS

    Подключая внешние CSS и Javascript, мы хотим снизить до минимума лишние HTTP-запросы.

    Для этого .js и .css файлы отдаются с заголовками, обеспечивающими надежное кеширование.

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

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

    Простое кеширование ETag

    Самый простой способ кеширования статических ресурсов — использование ETag.

    Достаточно включить соответствующую настройку сервера (для Apache включена по умолчанию) — и к каждому файлу в заголовках будет даваться ETag — хеш, который зависит от времени обновления, размера файла и (на inode-based файловых системах) inode.

    Браузер кеширует такой файл и при последующих запросах указывет заголовок If-None-Match с ETag кешированного документа. Получив такой заголовок, сервер может ответить кодом 304 — и тогда документ будет взят из кеша.

    Выглядит это так:

    Первый запрос к серверу (кеш чистый)

    GET /misc/pack.js HTTP/1.1
    Host: javascript.ru

    Вообще, браузер обычно добавляет еще пачку заголовоков типа User-Agent, Accept и т.п. Для краткости они порезаны.

    Ответ сервераСервер посылает в ответ документ c кодом 200 и ETag:

    HTTP/1.x 200 OK
    Content-Encoding: gzip
    Content-Type: text/javascript; charset=utf-8
    Etag: "3272221997"
    Accept-Ranges: bytes
    Content-Length: 23321
    Date: Fri, 02 May 2008 17:22:46 GMT
    Server: lighttpd

    Следующий запрос браузера
    При следующем запросе браузер добавляет If-None-Match: (кешированный ETag):

    GET /misc/pack.js HTTP/1.1
    Host: javascript.ru
    If-None-Match: "453700005"

    Ответ сервера
    Сервер смотрит — ага, документ не изменился. Значит можно выдать код 304 и не посылать документ заново.

    HTTP/1.x 304 Not Modified
    Content-Encoding: gzip
    Etag: "453700005"
    Content-Type: text/javascript; charset=utf-8
    Accept-Ranges: bytes
    Date: Tue, 15 Apr 2008 10:17:11 GMT

    Альтернативный вариант — если документ изменился, тогда сервер просто посылает 200 с новым ETag.

    Аналогичным образом работает связка Last-Modified + If-Modified-Since:

    1. сервер посылает дату последней модификации в заголовке Last-Modified (вместо ETag)
    2. браузер посылает дату закешированной версии в заголовке If-Modified-Since(вместо If-None-Match)
    3. Если скрипт не изменился — ответ содержит только код 304

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

    Умное кеширование. Версионность

    Общий подход для версионности — в двух словах:

    1. Во все скрипты добавляется версия (или дата модификации). Например, http://javascript.ru/my.js превратится в http://javascript.ru/my.v1.2.js
    2. Все скрипты жестко кешируются браузером
    3. При обновлении скрипта версия меняется на новую: http://javascript.ru/my.v2.0.js
    4. Адрес изменился, поэтому браузер запросит и закеширует файл заново
    5. Старая версия 1.2 постепенно выпадет из кеша

    Дальше мы разберем, как сделать этот процесс автоматическим и прозрачным.

    Жесткое кеширование

    Жесткое кеширование — своего рода кувалда которая полностью прибивает запросы к серверу для кешированных документов.

    Для этого достаточно добавить заголовки Expires и Cache-Control: max-age.

    Например, чтобы закешировать на 365 дней в PHP:

    header("Expires: ".gmdate("D, d M Y H:i:s", time()+86400*365)." GMT");
    header("Cache-Control: max-age="+86400*365);

    Или можно закешировать контент надолго, используя mod_header в Apache:

    Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT"
    Header add "Cache-Control" "max-age=315360000"

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

    Большинство браузеров (Opera, Internet Explorer 6+, Safari) НЕ кешируют документы, если в адресе есть вопросительный знак, т.к считают их динамическими. Именно поэтому мы добавляем версию в имя файла. Конечно, с такими адресами приходится использовать решение типа mod_rewrite, мы это рассмотрим дальше в статье.P.S А вот Firefox кеширует адреса с вопросительными знаками.

    Автоматическое преобразование имен

    Разберем, как автоматически и прозрачно менять версии, не переименовывая при этом сам файлы.

    Имя с версией -> Файл

    Самое простое — это превратить имя с версией в оригинальное имя файла.

    На уровне Apache это можно сделать mod_rewrite:

    RewriteEngine on
    RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$    /$1$2    [L]

    Такое правило обрабатывает все css/js/gif/png/jpg-файлы, вырезая из имени версию.

    Например:

    /images/logo.v2.gif -> /images/logo.gif
    /css/style.v1.27.css -> /css/style.css
    /javascript/script.v6.js -> /javascript/script.js

    Но кроме вырезания версии — надо еще добавлять заголовки жесткого кеширования к файлам. Для этого используются директивы mod_header:

    Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT"
    Header add "Cache-Control" "max-age=315360000"

    А все вместе реализует вот такой апачевый конфиг:

    RewriteEngine on
    # убирает версию, и заодно ставит переменную что файл версионный
    RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1]
     
    # жестко кешируем версионные файлы
    Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILE
    Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE

    Из-за порядка работы модуля mod_rewrite, RewriteRule нужно поставить в основной конфигурационный файл httpd.conf или в подключаемые к нему(include) файлы, но ни в коем случае не в .htaccess, иначе команды Header будут запущены первыми, до того, как установлена переменная VERSIONED_FILE. Директивы Header могут быть где угодно, даже в .htaccess — без разницы.

    Автоматическое добавление версии в имя файла на HTML-странице

    Как ставить версию в имя скрипта — зависит от Вашей шаблонной системы и, вообще, способа добавлять скрипты (стили и т.п.).

    Например, при использовании даты модификации в качестве версии и шаблонизатора Smarty — ссылки можно ставить так:

    <link href="{version src='/css/group.css'}" rel="stylesheet" type="text/css" />

    Функция version добавляет версию:

    function smarty_version($args){
     
      $stat = stat($GLOBALS['config']['site_root'].$args['src']);
      $version = $stat['mtime'];
     
      echo preg_replace('!\.([a-z]+?)$!', ".v$version.\$1", $args['src']);
    }

    Результат на странице:

    <link href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />

    Отпимизация

    Чтобы избежать лишних вызовов stat, можно хранить массив со списком текущих версий в отдельной переменной

    $versions['css'] = array(
      'group.css' => '1.1',
      'other.css' => '3.0',
    }

    В этом случае в HTML просто подставляется текущая версия из массива.

    Можно скрестить оба подхода, и выдавать во время разработки версию по дате модификации — для актуальности, а в продакшн — версию из массива, для производительности.

    Применимость

    Такой способ кеширования работает везде, включая Javascript, CSS, изображения, flash-ролики и т.п.

    Он полезен всегда, когда документ изменяется, но в браузере всегда должна быть текущая актуальная версия.

    By sysadmin on 25.02.2009 | Development
    Метки: , , , , , ,

    Введение в написание модулей ядра 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, написанную Дэниэлом Дрэйком.

    By sysadmin on 18.02.2009 | Development, Linux
    Метки: , , , , , , ,

    Изучаем параметры 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).

    By sysadmin on | Development
    Метки: , , , ,

    Опасный код на C

    Перевод цикла статей David Chisnall «Writing Insecure C, Part 1», «Writing Insecure C, Part 2», «Writing Insecure C, Part 3».

    Использование языка программирования C часто приводит к написанию очень опасного кода. Но это не совсем справедливое обвинение; такие проекты, как OpenBSD показывают, что возможно писать безопасный код на C. Проблема C та же, что и в ассемблере — язык открывает вам все возможности архитектуры, но и кое-что ещё. Он дает все возможности для написания безопасного кода, но не делает эти вещи сам.

    В этой статье рассмотрены стандартные примеры ошибок в коде C и то, как их и избежать.

    Проверка ошибок

    Множество современных языков включают в себя некоторый механизм по обработке исключений. Вообще, исключения — это плохая идея. Они усложняют управление ходом программы и у них есть большинство недостатков, от которых страдали программы с GOTO перед рассветом структурного программирования. Тем не менее, у исключений есть одно важное преимущество: вы не можете их игнорировать.

    В частности, код на Java часто засорен блоками try…catch, которые ничего не делают, кроме отбрасывания ошибок, но даже в этом случае механизм исключений преследует цель: это заставляет программиста помнить о том, что он небезопасно обрабатывает условия ошибок.

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

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

    Стандартный пример функции, которая почти никогда не завершается с ошибкой — malloc(), наряду со связанными с ней функциями, вроде calloc(). В спецификации C указано, что malloc должна вернуть NULL в случае, если памяти недостаточно, чтобы удовлетворить запрос. Linux не полностью следует этому правилу: он возвращает NULL, если в системе нет достаточного виртуального пространства адресов, чтобы выполнить выделение памяти, но в случае недостатка памяти, Linux по-прежнему выделяет пространство адресов, а затем возвращает ошибку, если вы пытаетесь использовать эту память. Как бы то ни было, исходя из того, что у вас есть подходящая реализация C, необходимо проверять значения, которые возвращает malloc.

    В большинстве случаев, вы не сможете сделать ничего разумного, если malloc завершится с ошибкой. Даже код, восстанавливающий ошибку, обычно нуждается в выделении памяти. Вы можете попробовать выделять эту память, когда программа запускается (не забудьте проверить, что вы можете получать к ней доступ). В качестве альтернативы, вы можете использовать что-нибудь вроде этого макроса:

    #define MALLOC(x,y) do { y = malloc(x); if (!y) abort(1); } while(0)

    Он будет тестировать каждое выделение памяти, и прерывать программу в случае ошибки. Вы можете заменить вызов abort на ваш код, обрабатывающий ошибку, но будьте осторожны. Одна из недавних уязвимостей в OpenSSH была вызвана вызовом кода, восстанавливающего ошибку в ситуации, когда программа была в неопределенном состоянии. Зачастую, прерывание выполнения безопаснее.

    Точно также важна проверка возвращаемых значений других функций.

    Начальные значения

    Если вы объявляете глобальную переменную в C, она безоговорочно инициализируется нулем. Этот способ очень удобный, и был бы ещё более удобным, если бы вы могли верить, что автор вашего компилятора прочитал эту часть спецификации. Как бы то ни было, если вы получаете память из любого другого источника, это правило не работает.

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

    Здесь существуют две потенциальные проблемы. Первая в том, что невозможно предсказать поведение неинициализированной переменной. Вторая в том, что этого может и не быть. В общем, использование значения перед инициализацией приведет к неопределенному состоянию вашей программы. В худшем случае, это может привести к утечке информации. Например, если вы пишете некоторый код, который использует неинициализированную переменную после вызова функции шифрования, ваше «случайное» значение неопределенной переменной может содержать некоторую часть ключа шифрования. Если ваш код запускает скрипты, которые могут читать информацию из стека, это может быть серьезной уязвимостью.

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

    int a = 42;

    А затем вы решаете, что вам необходимо условие для инициализации, поэтому вы копируете и вставляете его в выражение if:

    if ({некоторое условие})
     
    {
     
       int a = 42;
     
    }

    Ой, вы же забыли объявить переменную за пределами блока if, поэтому вы делаете это позже, и даете ей значение по умолчанию:

    int a = 0;
     
    if ({некоторое условие})
     
    {
     
       int a = 42;
     
    }

    Теперь у вас есть код, который компилируется и запускается. Большинство времени (когда не встречается условие), он будет работать. Но код внутри фигурных скобок будет «холостым». Он определяет новую переменную под названием a и присваивает ей 42. Как только блок закончится, эта переменная выйдет из области видимости и останется старая a, до сих пор со значением 0.

    Более незаметный вариант может возникнуть при опечатке в инициализации, когда вы выполняете что-то вроде этого:

    int value = value + 12;

    когда на самом деле ожидаете выполнение этого:

    int value = othervalue + 12;

    Как только компилятор распарсит int value, переменная станет валидной и будет находиться в области видимости. Вы можете считать её значение, добавить 12 к нему и присвоить результат ей же. К несчастью для вас, value, которую вы считываете, не определена. Теперь вам придется присвоить value неопределенное значение. Если вы считываете неосторожно, вы можете подумать, что проинициализировали её, хотя вы этого и не сделали. Компилятор с оптимизацией удалит +12, поскольку неопределенность плюс 12 есть неопределенность, и это будет эквивалентно следующему:

    int value;

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

    int a;
     
    call_some_function(&amp;a);

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

    Проблемы целых

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

    В высокоуровневых языках, числа обычно условно-точные, причем точность определена так, как вам надо. В C есть набор целых типов. Наиболее часто используемый — это int, который должен соответствовать машинному слову. На компьютерах, на которых я учил C, оно было длиной в 16 бит. Теперь он обычно размером в 32 бита, даже на архитектурах, где машинное слово имеет длину 64 бита, так как большое количество написанного кода подразумевает, что он всегда имеет длину 32 бита.

    Одна из наиболее частых причин странного поведения — попытка хранить значение указателя в int. На обычных 32-битных платформах, этот метод работает хорошо. На некоторых 64-битных он работает, а на некоторых — нет. Стандарт C99 определяет новый целый тип, intptr_t, который гарантированно имеет достаточный размер, чтобы хранить указатель. Если вы планируете хранить указатель в int, вы всегда должны использовать intptr_t.

    Указатель — один из других слегка сбивающих с толку моментов. C определяет два типа указателей: один указывает на код, другой указывает на данные. Указатель на функцию не имеет гарантировано такой же размер, что и указатель, указывающий на данные. На множестве встроенных платформ, часто используются 16-битные указатели для кода и 32-битные для данных. Преобразование типа в void* указателя на функцию приведет к тому, что некоторые данные будут отброшены.

    Другим основным видом целого в C является char, short и long. В C99 также определен long long. Ни один из них не имеет фиксированного размера, но все они имеют минимальные размеры (технически у них есть минимальный набор значений, который они могут хранить, не предоставляя гарантий по внутреннему формату). Short должен быть, по крайней мере, 16 бит, long как минимум 32, а lon long как минимум 64. Если вам необходимо минимальное количество точности, выберите один из них, и вы сможете получить дополнительное пространство к тому, что запросили, в зависимости от архитектуры.

    Я не упомянул char ещё и потому, что они детально отличаются от других типов. Все другие основные целые типы данных являются знаковыми, пока не будут объявлены, как беззнаковые. Это не всегда так в случае с char. К сожалению, в случае с char, знаковая она, или нет, полностью зависит от реализации. Если вы используете char, всегда явным образом объявляйте их как знаковые или нет, так как в противном случае, вы можете быть удивлены позднее.

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

     a = b + c;

    Так как вы храните результат в a, вы можете предположить, что сложение будет выполнено, каким бы типом не была a. По факту, оно будет выполнено с типом b и c. Это имеет смысл тогда, когда вы думаете, что лучше иметь значение (b + c) в зависимости от чего-то, чем b и c сами по себе. Вы можете думать, что тип этого выражения будет типом b. Стандарт C99 определяет набор правил для определения типа, но в основном он будет представлять собой тот тип, который больше, b или c. Например, если a — char, а b — int, тип выражения будет int. Поразительно часто встречается ошибка, когда делают что-то вроде такого:

    long long a;
     
    long b;// = чему-то
     
    long c;// = чему-то
     
    a = b * c;

    a как минимум 64 бита, b и c как минимум 32 бита. Так как у вас есть только гарантия того, что b и c по 32 бита, вы не должны помещать нечто большее в них, даже если ваш компилятор или платформа имеет long в 64 бита. Когда вы умножаете их, вы получаете результат, подходящий для 64-битного int и вы храните его в чем-то, что подходит для 64-битного int. Звучит удобно? Так и есть, кроме того факта, что результат искажается до 32 бит прямо перед присваиванием. Вот правильный способ сделать это:

    a = (long long)b * c;

    Это расширяет b до 64 бит (или больше, в зависимости от реализации). Расширение типа гарантирует, что c имеет тип такого же размера, что и b, так что она также расширена. Тогда умножение происходит как умножение двух long long с 32 или более первыми нулями, а результат (long long) хранится в переменной типа long long.

    В основном, вы можете избежать сюрпризов путем прямого преобразования типов к чему-то с достаточной точностью перед выполнением операции. Убедитесь, что отсутствие знака выполняется для всех переменных. Это очень частая причина ошибок, так как вы неожиданно теряете первый бит при преобразовании большого беззнакового значения к его знаковому эквиваленту.

    Более неуловимая ошибка возникает от переполнения int. Она особенна часто возникает при использовании malloc, так как стандартный шаблон написания — malloc(i * sizeof(x)). Если у взломщика есть влияние на i, он может попытаться выполнить это переполнение. Для очень больших значений i, это даст результат гораздо меньший, чем i, что приведет к проблеме. Вызов malloc будет успешным, но когда вы попытаетесь использовать массив, полученный в результате, только первые несколько значений будут валидными. Взломщик может вынудить вас переписать другие данные.

    Простым путем избегания этого вида уязвимости может быть использование calloc() вместо malloc() (конечно, в надежде, что реализация calloc в вашей системе производит проверку границ сама, а не просто делает malloc() и memset() для количество*размер байт).

    realloc() — ещё одна проблема. Нет стандартного пути сделать это с ней, поэтому вам надо делать это самому. К счастью, OpenSSH включает в себя xrealloc(), который является версией realloc() с проверкой границ. Она включает несколько других проверок, но если вам не нужны все из них, вы можете реализовать упрощенную версию:

    void * xrealloc(void *ptr, size_t nmemb, size_t size)
     
    {
     
        void *new_ptr;
     
        size_t new_size = nmemb * size;
     
        if (SIZE_T_MAX / nmemb &lt; size)
     
                return NULL;
     
            return realloc(ptr, new_size);
     
    }

    Этот тест довольно просто. SIZE_T_MAX — это максимальное значение, которое может иметь size_t. Когда вы делите на указанное количество элементов, вы получаете максимальный размер, который может быть без переполнения. Если этот размер меньше, чем требуемое пространство, возникает переполнение, поэтому вы возвращаете NULL.

    realloc возвращает NULL в случае ошибки, так что вам всегда следует проверять возвращаемое значение realloc на валидность. К сожалению, это является наиболее частой причиной утечек памяти (которые, в свою очередь, являются причиной атак DDoS). Если realloc() возвращает NULL, исходный указатель по-прежнему является валидным. Часто разработчики забывают этот принцип и просто делают что-то вроде этого:

    ptr = realloc(ptr, newsize);

    Когда realloc() возвращает NULL, вы теряете вашу ссылку на предыдущее выделение памяти. FreeBSD предоставляет удобную функцию, reallocf(), которая эквивалентна следующей:

    void *reallocf(void* ptr, size_t size)
     
    {
     
        void *newptr = realloc(ptr, size);
     
        if (NULL == newptr)
     
        {
     
            free(ptr);
     
        }
     
        return newptr;
     
    }

    Если у вас нет кода для обработки случая, когда realloc() завершается с ошибкой, вам необходимо использовать что-то вроде неё.

    Проблемы с памятью

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

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

    Что ещё хуже, это не работает для всех динамически выделяемых данных. Рассмотрим что-нибудь, вроде sprintf(). Этот аналог printf() пишет в буфер вместо стандартного вывода. Проблема sprintf в том, что вызывающей функции необходимо знать размер буфера, что не всегда просто — на самом деле, это требует реализации большинства кода sprintf в вызывающей функции.

    По факту, почти невозможно использовать sprintf безопасно. Вам необходимо указать длину каждого отдельного элемента строки формата (вот почему sprintf была создана). Это может привести к завершению процесса (что может также вызвать проблемы безопасности, и мы вернемся к этому позднее), поэтому некоторые реализации libc включают в себя asprintf().

    Функция asprintf() — это то же самое, что и sprintf, кроме того, что она выделяет свой собственный буфер с помощью malloc. Это позволяет избежать досрочного завершения процесса или переполнения. К несчастью, она приносит новую проблему: когда вызывающая функция должна освобождать указатели, которые возвращает вызываемая функция?

    Большое количество кода на C содержит в себе эту проблему. Решение обычно заключается в том, чтобы поместить в документацию о функции строку «вызывающая функция должна освободить возвращаемый указатель». К сожалению, этот подход делает сложной возможность взглянуть на часть кода и увидеть, валидна ли она. Одно из решений заключается в том, чтобы заключать в оболочку каждый указатель, например, так:

    typedef struct _RefCountedBuffer
     
    {
     
        void *buffer;
     
        int refcount;
     
        void (free*)(struct _RefCountedBuffer*);
     
    } *RefCountedBuffer;

    Когда вы возвращаете указатель, вы создаете одну из этих структур и устанавливаете refcount в 1. Когда вы получаете его, вы всегда вызываете функцию, которая увеличивает refcount на единицу и вызывает соответствующую функцию free(), если она достигнет нуля.

    Эта методика близка для программистов Objective-C, потому как OpenStep реализует схожий механизм работы. GNUstep продвинулся дальше, предоставляя макросы ASSIGN() и DESTROY(). Эти макросы делают ошибки при работе с памятью более редкими, и мы можем сделать нечто похожее на обычном C.

    Прежде всего, нам надо определить функции retain() и release():

    RefCountedBuffer retain(RefCountedBuffer *buf)
     
    {
     
        buffer->refcount++;
     
        return buffer;
     
    }
     
    void release(RefCountedBuffer *buf)
     
    {
     
        buf->refcount--;
     
        if (buf->refcount == 0)
     
            buf->free(buf);
     
    }

    Учтите, что это упрощенные версии тех функций, что вам могут действительно потребоваться. Наиболее распространенная проблема — они не являются потоко-безопасными. Операторы ++ и — обычно компилируются в последовательность команд загрузить, добавить (или вычесть), сохранить. Если два потока, например, одновременно аккумулируют, они оба должны сначала загрузить эти значения, прежде чем сохранить их, и одна аккумуляция будет потеряна. Вы можете обойти эту проблему, используя специальный ассемблер для ЦПУ или встроенные средства GCC для элементарных операций.

    Определив эти функции, вы можете определить макросы SET и FREE следующим образом:

    #define SET(var, val) do { 
     
    RefCountedBuffer __tmp = retain(val); 
     
    if (NULL != var) release(var) var = __tmp; } while(0)

    Учтите, что вы удерживаете новое значение перед освобождением старого, что может привести к проблемам в случае, если новое и старое значения равны. Соответствующий макрос FREE() довольно прост:

    #define FREE(var) do { release(var) var = NULL; } while(0)

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

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

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

    Тем не менее, эта модель не решает нашу первоначальную проблему функций, похожих на asprintf. Она требует вернуть строку, что часто используется только в вызывающей функции. Для этого выделение памяти в куче и заключения её в структуру подсчета ссылок будет лишним. Вам нужен способ выделить пространство в стековом фрейме вызывающей функции.

    У разработчиков сервера Dovecot IMAP есть превосходное решение. В дополнение к обычному контролю стека, они делают отдельный стек данных и аналог asprintf, которая использует его. Вызывающая функция сначала вызывает функцию, которая выделяет фрейм в новом стеке данных. Затем она вызывает аналог asprintf(), которая выделяет пространство в стеке результатов. Этот подход работает правильно, пока вызывающая функция достает верхний фрейм стека данных.

    Так как контрольный стек и стек данных работают на разных скоростях, вы можете просто вернуть данные в стек данных. Пока функция, которая создала фрейм в стеке данных, также и уничтожает его, у вас не будет проблем. Работа организована таким образом, что сначала создается фрейм в стеке данных, а затем вызывается asprintf(), которая выделяет пространство в текущем фрейме стека данных и помещает результат в него. Вы используете результат, а затем выталкиваете из стека текущий фрейм.

    Вам ничего не мешает иметь несколько независимых областей памяти. Вы можете использовать mmap() на /dev/zero, чтобы выделить где-либо непрерывный BLOB памяти и использовать его по своему желанию. Одной из возможных идей может быть выделение стека данных, который работает на такой же скорости, как и контрольный стек. Используйте этот метод для всех массивов, которые вы будете по-другому выделять в стеке. В отличие от контрольного стека, это может расти вверх в памяти. Вы можете сделать его переместимым, постоянно адресуя его через глобальный указатель к началу. Например, с помощью подобного макроса:

    #define NEW_ARRAY(type, name, elements) 
     
    __attribute__((cleanup(popDataStack)))    
     
    type *name = dataStackCalloc(sizeof(type), elements)

    __attribute__(cleanup) — это расширение GCC. Оно вызывает функцию popDataStack() с указателем на переменную в качестве аргумента, когда переменная выходит из области видимости. Теперь у вас есть указатель на что-то в стеке данных. Вместо использования прямого адреса, вы можете использовать макрос, который добавляет этот адрес указателю. Все это позволяет продолжать расти вашему стеку данных до тех пор, пока у вас есть достаточное количество непрерывной свободной памяти для хранения.

    Как бы то ни было, вы до сих пор можете выйти за пределы массива. Вы не разрушите возвращаемый адрес, но можете переписать некоторые данные или могут возникнуть другие проблемы. Вы можете избежать выхода за пределы стека данных, вызвав функцию mprotect() на последней странице стека, чтобы удалить все права доступа. У большинства реализаций malloc() есть режим отладки, который вставляет этот вид защитной страницы после каждого выделения памяти. Вы можете сделать это, создав свой стек данных, в котором будут чередоваться доступные и недоступные страницы, с каждым выделенным массивом, так что он завершится недоступной страницей, но эта система достаточно ресурсоемка. Никакая нормальная операционная система не выделит реальную память для защитных страниц, но вы потеряете много пространства в промежутках между началами страниц и началами массивов и будете использовать большое количество адресного пространства.

    Буферы и строки

    Строки в C — вечная причина проблем. Когда C был создан, было две концепции о том, как лучше всего реализовывать строки — известные сейчас как строки C и строки Pascal, в соответствии с языками, которые сделали эти идеи популярными. Такие языки, как Lisp использовали третью реализацию: строки являлись связанным списком символов (Erlang до сих пор использует эту модель).

    Строки в стиле Lisp имеют очевидный недостаток. Каждому символу требуется один байт для хранения символа и 4 или 8 байт для хранения адреса следующего, — до девяти байт уходит на хранение одного байта данных. Эта структура далека от идеальной, но делает разделение и конкатенацию строк очень простой.

    Более совершенные модели представляют строки, как связанный список массивов символов, позволяя легко их объединять.

    Все эти модели могут быть (и это было сделано) реализованы на C, но стандартные строковые функции до сих пор работают с массивами байтов.

    Большинство «классических» строковых функций практически невозможно использовать безопасно (по этой причине линковщик OpenBSD легко выдает предупреждение, когда вы используете одну из них). Каноническим примером «плохой» функции является strcat(), которая принимает два указателя на строки C. Функция проходит по первой строке, пока не найдет null; она записывает туда байты из второй строки пока не дойдет до null во второй строке. Вызывающая функция должна быть уверена, что существует достаточно места в первой строке, чтобы сохранить вторую.

    Более новая функция, strncat(), была создана, чтобы сделать это легче. Эта функция принимает в качестве третьего аргумента объем пространства в первой строке. Она гарантирует, что функция никогда не выйдет за пределы первой строки, но создает новую проблему: функция возвращает новую строку в качестве результата, поэтому вы не можете легко протестировать, был ли искажен результат. Это становится большой проблемой, когда соединяемые части являются, например, паролем.

    В OpenBSD представлена strlcat, которая похожа на strncat, но возвращает сумму входных строк. Если результат работы функции больше третьего аргумента, имело место искажение. Эта функция находится в каждой ОС семейства BSD (включая Darwin/OS X), но её нет в glibc, так как, согласно главному разработчику glibc, является «бесполезным хламом BSD». К счастью, лицензия BSD позволяет вам копировать эту функцию из libc BSD в ваш код без каких-либо проблем.

    Проблемы со строками в C велики из-за того факта, что строки — это просто массивы без проверки границ. Точно также, большинство проблем, затрагивающих строки, касаются и произвольных буферов.

    Одной из самых опасных вещей в C99 является модель массивов переменной длины, которая позволяет вам выделять маленькие, с динамическим размером массивы в стеке. Вы можете всегда делать это с помощью alloca(), хотя качество реализаций alloca() варьируется между разными платформами. Следующие примерно эквивалентны:

    int *a = alloca(sizeof(int) * n);
     
    int a[n];

    Разница в поведении, если существует недостаточно пространства для роста стека до n целых значений. Первая строка будет при этом NULL, но вы сможете проверить это, что упрощает отладку — достаточно просто обратиться к началу массива. Во второй строке, если не будет хватать размера стека, он будет указывать… куда-то. А куда именно, будет зависеть от реализации. Вот почему, если вы используете массивы C99 с переменной длиной, невозможно протестировать программу на переполнение стека. В большинстве случаев, это не является проблемой. Небольшие выделения памяти более безопасны для работы, но если взломщик сможет увеличить размер n, у вас может получиться массив, указывающий никуда.

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

    Масштаб данной проблемы уменьшен в современных операционных системах (они проверяют целостность возвращаемого адреса и завершают процесс, если он невалиден), но это по-прежнему плохое решение. Сбои в программе лучше, чем удаленные эксплоиты, но нет ничего лучше валидного кода.

    Когда все идет не так

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

    Ключом к безопасному программированию каждой части вашей программы является представление, что другая часть была написана идиотом. Вы можете проверить, что каждый входящий указатель — не NULL (хотя печально, что C не предоставляет какого-либо механизма для проверки, что он указывает на валидную часть памяти). Вы можете проверить, что любые другие значение находятся в ожидаемых вами рамках. Тем не менее, в конце концов, баг в одной части процесса может разрушить данные — а во многих операционных системах, даже код — всей остальной части процесса.

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

    Разбивая компоненты программы на отдельные процессы, вы ограничиваете размеры разрушений, которые может принести один баг. Самым распространенным примером является разделение привилегий кода, находящееся во множестве серверных программ. Многие сервера должны быть запущены под root, или с соответствующими правами. Им нужна эта возможность для привязки к соответствующим портам и осуществления других действий в качестве root — таких, как получения доступа к данным разных пользователей или запуск программы под другим пользователем.

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

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

    Отказ от привилегий

    Хотя разделение привилегий работает хорошо, многое может быть сделано благодаря простому отказу от привилегий. UNIX содержит семейство системных вызовов setuid(), которые позволяют процессу, запущенному в качестве root, работать как другой пользователь.

    Веб-сервер нуждается в запуске в качестве root, так как ему нужно быть привязанным к 80 порту и иметь доступ к файлам в директории public_html каждого пользователя. Тем не менее, как только он привязан к 80 порту, ему не нужно более работать как root, и он может отказаться от прав root. Хотя ему по-прежнему нужен способ получать доступ к директории public_html каждого пользователя. Одно решение — заставить каждого пользователя сделать его файлы доступными для группы веб-сервера. Другим может быть выполнение fork() процесса-потомка для каждого пользователя, который работает в качестве этого пользователя и имеет доступ к файлам в директории пользователя.

    Немного повысить безопасность можно, используя системный вызов chroot(), который меняет корневую директорию (которая видна процессу) на специальную директорию. Любые файлы за пределами этой директории находятся вне зоны видимости, хотя к тем, что были открыты, доступ по-прежнему остается. Этот факт важен, так как вы можете держать библиотеки и даже исполняемую программу, а также файлы настройки вне «тюрьмы» chroot.

    Пользователь с правами root может легко избежать chroot, создав новое устройство для жесткого диска и смонтировав его внутрь chroot (или даже получая доступ к нему напрямую). Вот почему важно сбросить привилегии root сразу же после входа в chroot.

    Ещё легче использовать chroot(), если он встроен в приложение. Команда chroot, которая запускает процесс в окружении chroot, также доступна, но этот подход имеет две проблемы. Первая — он вызывает chroot до запуска процесса, поэтому программа и все необходимые библиотеки должны быть внутри директории chroot. Вторая заключается в том, что он должен выполняться в качестве root, поэтому ему нужно что-то внутри chroot для возможности сброса привилегий. Обычным решением является поместить команду su внутрь chroot. Хотя, когда вы поместите так много кода внутрь chroot, он станет выглядеть, как и внешнее окружение.

    Разгоняя ядро

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

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

    1. Пользовательский процесс запрашивает системный вызов.

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

    3. Ядро обрабатывает вызов.

    К сожалению, этот подход имеет немного неожиданное поведение. Многие системные вызовы принимают указатели в качестве аргументов. Обычно ядро отображается в адресное пространство каждого процесса (но в режиме привилегий помечается только как для чтения), поэтому обработчик системного вызова в ядре может получить доступ к тому, на что указывают эти указатели дешево (без копирования). Даже если он не может получить к ним доступ напрямую на платформах, где ядро имеет полностью отдельное адресное пространство, обычно оно так же может получать доступ к адресному пространству процесса дешево. Если вы производите системный вызов с указателем, вот добавочный шаг 2a:

    2a. Другой поток изменяет данные, на которые указывает аргумент-указатель.

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

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

    Заключение

    Писать безопасный код на C сложно, но это возможно. Безопасность таких систем, как OpenBSD доказывает, что это можно сделать. Язык не делает написание безопасного кода простым, но в некоторых случаях этот факт является полезным. Чтобы избежать проблем, программист должен основываться на хорошем коде, а не на возможностях языка.

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

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

    By sysadmin on | Development
    Метки: , , ,

    Регулярные выражения в JavaScript

    Если вам нужно использовать регулярные выражения при создании сайта, вовсе не обязательно прибегать к использованию Perl или PHP, — JavaScript вполне может справится с этой работой. В этой статье мы рассмотрим объект RegExp в JavaScript и его методы. Я уверен, что после прочтения, JaveScript представится вам в другом свете.

    Настоящий Мир

    Большинство Настоящих Программистов относятся к JavaScript, словно к бедной родственнице из деревни — иногда полезна, но не очень важна. Настоящих Программистов не интересует язык, чье предназначение в том, чтобы заменять одно изображение другим или рисовать след за мышью по веб-странице. У Настоящих Программистов есть более важные дела.

    Что ж, JavaScript ограничен стороной клиента в веб-транзакции, но на этой территории, он безусловный король. Нет языка, настолько простого к изучению или дающего возможность сделать столько полезных вещей с минимальными усилиями. И одно из того, что вы можете делать, это использовать регулярные выражения.

    По ходу этой статьи, я собираюсь дать вам представление о регулярных выражениях в контексте JavaScript. Я покажу, как использовать объект String в JavaScript для простейших возможностей поиска соответствия или осуществления замены, так же, как и для более сложных действий со строками. И я представлю вам объект RegExp, который предоставляет удобный путь для создания более эффективного кода распространенной проверки входящих данных на стороне клиента. Итак, приступим; независимо от того, Настоящий ли вы Программист, или только пытаетесь им стать, я уверен, что вы найдете для себя кое-что полезное ниже.

    Входим в Матрицу

    Регулярные выражения, также известные как «регэкспы» среди программистов — мощный инструмент, использующийся для поиска по шаблону и замены подстрок. Они тесно связаны с почти всеми основанными на *nix утилитами, включая редакторы, вроде vi, скриптовые языки вроде Perl или PHP и консольными программами, такими, как awk и sed.

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

    Например, одно из самых распространенных приложений регулярных выражений — проверка того, верен или нет введенным в форму пользователем адрес электронной почты; если да, форма будет принята, а если нет, появится сообщение, просящее пользователя исправить ошибку. Здесь регулярные выражения играют большую роль в процедурах интернет-приложений, принимающих решение, хотя, как вы увидите позже, они также могут иметь огромный эффект в сложных операциях поиска и замены.

    Регулярное выражение обычно имеет вид, подобный этому:

    /matrix/

    Нахождение шаблона «matrix» это все, что оно делает. Как насчет чего-то более сложного? Попробуйте это:

    /mat+/

    Это найдет слова «matting» и «mattres», но не «matrix». Почему? Потому что символ «+» используется для вхождения одного или более символа, расположенного перед ним. В нашем случае, за символами «ma» следует одно или более вхождение буквы «t».

    Также есть мета-символы (это официальный термин), похожие на «+»: «*» и «?». Они используются для обозначения от нуля и более количества вхождений и нуля или одного вхождения предыдущего символа, соответственно. Поэтому:

    /eg*/

    может найти «easy», «egocentric» и «egg», в то время, как

    /Wil?/

    может найти «Winne», «Wimpy», «Wilson» и «William», но не «Wendy» или «Wolf». Если все это будет немного неточным, вы можете также указать интервал чисел совпадений. Например, регулярное выражение

    /jim{2,6}/

    может найти «jimmy» и «jimmmmmy!», но не «jim». Числа в фигурных скобках указывают минимальное и максимальное значения интервала соответствия; вы можете не указывать верхний предел для бесконечного количества соответствий.

    Двое для Танго

    А сейчас вы узнаете, какие регулярные выражения есть, давайте посмотрим на их использование в скрипте. Объект String в JavaScript дает набор методов, которые поддерживают регулярные выражения. Первый из них — это метод search(), используемый для поиска строки для соответствия определенному регулярному выражению. Посмотрите на следующий пример:

    <script language="JavaScript">
     
    // определяем строку для поиска
     
    var str = "The Matrix";
     
    // определяем шаблон поиска
     
    var pattern = /trinity/;
     
    // ищем и возвращаем результат
     
    if(str.search(pattern) == -1) 
     
    {
     
       alert("Тринити не в Матрице");
     
    } else 
     
    {
     
       alert("Трините в Матрице на символе " + 
     
       str.search(pattern));
     
    }
     
    </script>

    Если вы выполните этот скрипт, вы увидете следующее:

    Тринити не в Матрице

    Метод search() возвращает позицию подстроки, соответствующую регулярному выражению или -1 в случае отсутствия такого соответствия. В нашем примере видно, что шаблона «trinity» в строке «The Matrix» нет, поэтому мы и получаем сообщение об ошибке. А теперь посмотрим, что будет, если изменить регулярное выражения так, чтобы результат был положительным:

    <script language="JavaScript">
     
    // определяем строку для поиска
     
    var str = "The Matrix";
     
    // определяем шаблон поиска
     
    var pattern = /tri/;
     
    // ищем и возвращаем результат
     
    if(str.search(pattern) == -1) 
     
    {
     
       alert("Тринити не в Матрице");
     
    } else 
     
    {
     
       alert("Трините в Матрице на символе " + 
     
       str.search(pattern));
     
    }
     
    </script>

    В этот раз, интерпретатор JavaScript найдет соответствие (и место, где его нашел). Вот результат:

    Trinity located in The Matrix at character 7

    Игра, Установка, Соответствие

    Объект String также предоставляет метод match(), который может расцениваться, как близкий родственник метода search(). В чем же разница? Что ж, вы уже увидели, что метод search() возвращает позицию, где находится соответствие. Метод match() работает немного по-другому: он применяет шаблон к строке и возвращает массив найденных значений.

    Смущены? Посмотрите на следующий пример:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "Mississippi";
     
    // определяем шаблон
     
    var pattern = /is./;
     
    // проверяем на вхождение
     
    // помещаем результат в массив
     
    var result = 
     
    str.match(pattern);
     
    // display matches
     
    for(i = 0; i < result.length; i++) 
     
    {
     
     alert("Соответствие #" + (i+1) + ": " + result[i]);
     
    }
     
    </script>

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

    Соответствие #1: iss

    В этом примере, я залал регулярное выражение «is.». Он найдет строку «is», за которой следует любой символ (оператор «.» в конце шаблона находит все что угодно в строке). Если вы посмотрите на строку, в которой мы производили поиск, вы увидите, что в ней есть два вхождения этого шаблона. В то время, как код возвращает только 1.

    Почему?

    Ответ прост — я «забыл» добавить модификатор «g» (для поиска одного и более вхождений) в шаблон. В отличие от следующего примера, который отличается от предыдущего тем, что в нем добавлен этот оператор:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "Mississippi";
     
    // определяем шаблон
     
    // и глобальный модификатор
     
    var pattern = /is./g;
     
    // проверяем на вхождение
     
    // помещаем результат в массив
     
    var result = 
     
    str.match(pattern);
     
    // display matches
     
    for(i = 0; i < result.length; i++) 
     
    {
     
     alert("Соответствие #" + (i+1) + ": " + result[i]);
     
    }
     
    </script>

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

    Найти и Уничтожить

    Предыдущий набор примеров демонстрировал возможности поиска объекта String. Но это ещё не все! Вы также можете осуществлять операции поиска и замены с помощью метод replace(), который принимает регулярное выражение и значение для его замены. Вот так:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "Welcome to the Matrix, Mr. Anderson";
     
    // заменяем одну строку на другую
     
    str 
     
    = str.replace(/Anderson/,"Smith");
     
    // показываем новую строку
     
    alert(str);
     
    </script>

    Если вы загрузите этот пример в браузере, вы увидите, что подстрока «Anderson» была занесена строкой «Smith». Что и иллюстрирует результат:

    Welcome to the Matrix, Mr. Smith

    Помните, как мы использовали модификатор «g» для поиска нескольких вхождений шаблона в строку? Переходим на следующую ступень — вы можете использовать его для замены нескольких вхождений шаблона в строку:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "yo ho ho and a bottle of gum";
     
    // возвращает "yoo hoo hoo and a bottle of gum"
     
    alert(str.replace(/os/g, "oo "));
     
    </script>

    Здесь мета-символ «s» обозначает пробелы после «yo» и «ho» и заменяет на «oo».

    Также, вы можете использовать нечувствительный к регистру поиск по шаблону — просто добавьте модификатор «i» в конце шаблона. Следующий пример демонстрирует, как это делается:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "he He hE HE";
     
    // возвращает ho ho ho ho
     
    alert(str.replace(/he/gi, "ho"));
     
    </script>

    Разделяя

    Объект String также предоставляет метод split(), который может быть использован для разделения одной строки на отдельные части на основе особого значения разделения. Эти части затем помещаются в массив для дальнейшей обработки. Демонстрирует это следующий пример:

    <script language="JavaScript">
     
    // определяем строку
     
    var friends = "Joey, Rachel, Monica, Chandler, Ross, 
     
    Phoebe";
     
    // разделяем на части с помощью запятых
     
    var arr = friends.split(", ");
     
    // проходим по массиву и выводим
     
    // каждое значение
     
    for (x=0; x < arr.length; x++)
     
    {
     
       alert("Hiya, " + arr[x]);
     
    }
     
    </script>

    В JavaScript версии 1.1 и ниже, вы можете использовать только строковые значения в качестве разделителей. JavaScript 1.2 меняет все это, теперь вы можете разделять строки даже на основе регулярных выражений.

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

    Neo
     
    | Trinity   |Morpheus    |  
     
    Smith|  Tank

    Здесь символ «|» используется для разделения различных имен. И количество пробелов между разными «|» разное — это означает, что прежде, чем вы сможете использовать разные элементы строки, вы вынуждены удалить лишние пробелы вокруг них. Разделение с использованием регулярного выражения в качестве разделителя является элегантным решением этой проблемы, что мы и видим на следующем примере:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "Neo| Trinity   
     
    |Morpheus    |  Smith|  Tank";
     
    // определяем шаблон
     
    var pattern = /s*|s*/;
     
    // разделяем строку с помощью
     
    // регулярного выражения в
     
    // качестве разделителя
     
    result = 
     
    str.split(pattern);
     
    // проходим получившийся массив
     
    for(i = 0; i < result.length; i++) 
     
    {
     
     alert("Символ #" + (i+1) + ": " + result[i]);
     
    }
     
    </script>

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

    Объекты в зеркале заднего вида

    Итак, все примеры в этой статье связаны с объектом String для демонстрации мощи реализации регулярных выражений в JavaScript. Но JavaScript также предоставляет базовый объект, RegExp, смысл существования которого — поиск по шаблону в строках и переменных.

    Этот объект имеет три полезных метода. Вот они:

    test() — проверяет строку на вхождение по шаблону.

    exec() — возвращает массив найденных вхождений в строке, позволяя расширенную работу с регулярными выражениями

    compile() — после того, как регулярное выражение связано с объектом RegExp.

    Давайте рассмотрим простой пример:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "The Matrix";
     
    // создаем объект RegExp
     
    var character = new RegExp("tri");
     
    // ищем по шаблону в строке
     
    if(character.test(str)) {
     
        alert("User 
     
              located in The Matrix.");
     
    } else {
     
        alert("Sorry, user is not in The 
     
               Matrix.");
     
    }
     
    </script>

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

    Основное отличие находится в том, что создается объект RegExp для поиска с помощью регулярного выражения. Он создается с помощью ключевого слова «new», следующего за конструктором объекта. По определению, конструктор принимает два параметра: шаблон для поиска и модификаторы, если они имеют место быть (в этом примере их нет).

    Следующим шагом после создания объекта, является его использование. Здесь мы использовали метод test() для поиска вхождения по шаблону. По умолчанию, этот метод принимает строковую переменную и сравнивает её с шаблоном, переданным в конструктор объекта. В случае нахождения соответствия, он возвращает true, в противном же случае false. Очевидно, что это более логичная реализация, чем использование метода search() объекта String.

    Раз Миссиссипи, два Миссиссипи…

    Следующий метод, который мы рассмотрим — это exec(). Поведение этого метода похоже на то, что делает метод match() объекта String. Посмотрим:

    <script language="JavaScript">
     
    // определяем строку
     
    var place = "Mississippi";
     
    // указываем шаблон
     
    var obj = /is./;
     
    // ищем вхождение,
     
    // помещаем результат в массив
     
    result = 
     
    obj.exec(place);
     
    // показываем результат
     
    if(result != null) {
     
     alert("Found " + result[0] 
     
    + " at " + result.index);
     
    }
     
    </script>

    Метод exec() возвращает соответствие указанному регулярному выражению, если такое имеется, как массив. Вы можете обратиться к первому элементу массива, чтобы получить найденную подстроку, а также её расположение с помощью метода index().

    Главное различие между методами match() и exec() в передаваемых параметрах. Первый требует шаблон в качестве аргумента, второй же требует строку для проверки.

    И это ещё не все. У метода exec() есть возможность продолжить поиск по строке для нахождения аналогичного вхождения без указания модификатора «g». Протестируем эту возможность с помощью следующего примера:

    <script language="JavaScript">
     
    // определяем строку
     
    var place = "Mississippi";
     
    // определяем шаблон
     
    var obj = /is./;
     
    // ищем все вхождения в строку
     
    // показываем результат
     
    while((result = 
     
       obj.exec(place)) != null) {
     
        alert("Found " + result[0] + " at " + 
     
              result.index);
     
    }
     
    </script>

    Итак, что у нас есть здесь? Для начинающих: я использовал цикл «while» для вызова метода exec() до тех пор, пока не достигнут конец строки (на котором объект вернет null и цикл закончится). Это возможно, потому что каждый раз, вызывая exec(), объект RegExp продолжит поиск с того места, на котором закончил.

    Но это все теория — такой код не будет работать ни в Internet Explorer, ни в NetScape Navigator, так что используйте его осторожно. Так что этот код чисто теоретический, по крайней мере до тех пор, пока не исправили ошибку (наверняка уже исправили — примеч. переводчика).

    Другая интересная особенность этого кода заключается в создании объекта RegExp. Вы наверняка заметили, что, в отличие от предыдущего примера, здесь не используется конструктор для создания объекта. Вместо этого, шаблон просто применяется к переменной. Думайте об этом просто как о более коротком способе создания объекта RegExp.

    Замены в ходе работы

    Вы могли заметить в предыдущих примерах использования объекта RegExp, что регулярное выражение указывается во время создания объекта. Поэтому вы можете заинтересоваться, а что если требуется поменять шаблон позже?

    Что ж, это не проблема. Метод compile() позволяет пользователю изменить шаблон объекта RegExp в выполнении поиска. Посмотрим:

    <script language="JavaScript">
     
    // определяем строку
     
    var str = "The Matrix";
     
    // определяем шаблон
     
    var pattern = "trinity";
     
    // define object
     
    var character = new RegExp(pattern);
     
    // смотрим вхождения
     
    if(character.test(str)) {
     
    alert("Looking for " + 
     
        pattern + "...User located in The Matrix");
     
    } else {
     
    alert("Looking for " 
     
        + pattern + "...Sorry, user is not in The Matrix");
     
    }
     
    // меняем шаблон
     
    var pattern = "tri";
     
    character.compile(pattern);
     
    // смотрим на вхождение и показываем результат
     
    if(character.test(str)) 
     
    {
     
       alert("Looking for " 
     
         + pattern + "...User located in The Matrix");
     
    } 
     
    else {
     
       alert("Looking for " + pattern 
     
         + "...Sorry, user is not in The Matrix");
     
    }
     
    </script>

    Учтите, что использование метода compile() для динамического изменения шаблона, связано с объектом RegExp.

    Работаем с формами

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

    <html>
     
    <head>
     
    <script 
     
    language="Javascript">
     
    // требует регулярное выражение
     
    // в качестве параметра
     
    function checkField(theForm, 
     
    theField, theFieldDisplay, objRegex) {
     
     objField = eval("document." + theForm + 
     
    		"." + theField);
     
     if(!objRegex.test(objField.value))  {
     
         alert ("Please 
     
               enter a valid " + 
     
    	   theFieldDisplay + "");
     
     objField.select();
     
     objField.focus();
     
     return (false);
     
     }
     
    return (true);
     
    }
     
    // регулярные выражения для различных
     
    // полей формы
     
    // номер кредитной карты
     
    // должен содержать 20 цифр
     
    objPatCCNum = 
     
    /^[0-9]{20}$/;       
     
    // дата окончания действия
     
    // кредитной карты
     
    // должна состоять из месяца 
     
    // от 01 до 12 и года от 2003 до 2010
     
    objPatCCDOE = 
     
    /^([0][1-9]|[1][1-2])/20(0[3-9]|10)$/; 
     
    // пин-код кредитной карты 
     
    // должен быть численным
     
    objPatCCPin = 
     
    /^[0-9]+$/;
     
    // адрес e-mail
     
    // должен быть в формате user@host
     
    objPatCCEmail 
     
    =
     
    /^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/;
     
    // проверяем различные поля формы
     
    function checkForm(theForm)
     
    {
     
    if(checkField(theForm, "cc_num", "Credit card number", objPatCCNum) 
     
    &&
     
    checkField(theForm, "cc_doe", "Date of expiry", objPatCCDOE) 
     
    &&
     
    checkField(theForm, "cc_pin", "PIN code", objPatCCPin) 
     
    &&
     
    checkField(theForm, "cc_email", "Email address", objPatCCEmail)) 
     
    {
     
     	return true;
     
    } else {
     
    	return false;
     
    }
     
    }
     
    </script>
     
    </head>
     
    <body>
     
    <h2>Информация о кредитной карте</h2>
     
    <form 
     
    name="frmCCValidation" 
     
    onSubmit="return checkForm('frmCCValidation');">
     
    Номер кредитной карты:<br>
     
    <input name="cc_num" type="text">
     
    <p>
     
    Тип кредитной карты <br>
     
    <select name="cc_type">
     
    <option 
     
    value="Visa">Visa</option>
     
    <option 
     
    value="Mastercard">Mastercard</option>
     
    <option 
     
    value="AmericanExpress">American 
     
    Express</option>
     
    </select>
     
    <p>
     
    Дата окончания действия (мм/гггг) <br>
     
    <input name="cc_doe" 
     
    type="text">
     
    <p>
     
    Пин-код <br>
     
    <input name="cc_pin" type="text">
     
    <p>
     
    Адрес электронной почты<br>
     
    <input name="cc_email" type="text">
     
    <p>
     
    <input type="submit" value="Отправить">
     
    </form>
     
    </body>
     
    </html>

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

    В заключение

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

    Я начинал с простого введения в регулярные выражения и быстро перешел к методам search() и replace() объекта String. Эти функции принимают регулярное выражение в качестве параметра и позволяют производить операции поиска и замены со строковыми переменными. Это привело нас к рассказу об объекте RegExp. Этот объект дает набор методов и свойств, позволяющих программистом повысить уровень мощности использования регулярных выражений в JavaScript.

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

    Вот несколько дополнительных ссылок, которые помогут вам понять концепцию регулярных выражений ближе:

    «Stringing Things Along»

    «Pattern Matching and Regular Expressions»

    «Regular Expressions for client-side JavaScript, a free online quick reference»