C. Глава 0. Hello, World!

Jan 25, 2018 00:14


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

(Б. Керниган, Д. Ритчи "Язык программирования C")

1. Терминал UNIX
После того, как вы убедились в том, что графический интерфейс в современных Linux-системах ничем не уступает Windows, можно начинать. Linux является клоном UNIX, пусть даже и не генетическим клоном. В UNIX есть свои особенности, но это очень простая операционная система. В деталях она, конечно, является очень сложной, но пользоваться ею проще и удобнее, чем Windows. Оценить это в самом начале бывает непросто. Лучше всего, пока не думать об этом и сразу же приступить к делу.

Если бы мы работали в Windows, то для изучения C нам пришлось бы устанавливать громоздкий пакет Visual Studio C++ за немалые деньги (лицензия стоит всего каких-то жалких $2,999 или 180 тысяч рублей в год). Теперь есть и бесплатные компиляторы. Но вместо C пришлось бы изучать кнопки в интерфейсе IDE, которых там больше, чем в кабине самолета. Этим путем идут 99.999% тех, кто изучает или сталкивается с языком C. В нашем случае все обстоит гораздо проще и эффективней. Средой разработки в нашем случае будет терминал, в котором мы будем иметь дело с командной строкой. Это тот редкий случай, когда работа по старинке оказывается намного эффективней.

Для новичков из пользователей Windows следует отметить пять важных особенностей UNIX. Во-первых, здесь нет понятия “буква диска”. Все устройства примонтированы к корню, обозначаемому просто /. Каталоги разделяются косой чертой /, слешем, а не обратной косой чертой \. Во-вторых, имеется чувствительность к регистру символов, файлы abc.txt и Abc.txt - это совсем разные файлы. В третьих, UNIX совершенно равнодушна к такой вещи, как расширение имени файла. Тип файла в большинстве случаев распознается системой по содержимому. В четвертых, UNIX изначально создавалась как система для работы с текстами, так что, очень многое в ней - это простые текстовые файлы. И в пятых, в системе действуют права доступа для каждого файла: на запись, чтение и исполнение. Это не все особенности, но об остальных мы поговорим позже.

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



Рис. 0-1. Окно терминала на Рабочем столе Xfce.

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

Давайте создадим место, где мы будем работать. Каждому пользователю в системе выделяется каталог /home/username (username - это ваш логин в системе, какой именно, вы знаете). Каталог username можно считать корнем пользовательской части (поддерева) файловой системы, где у пользователя username есть все права. О правах мы еще когда-нибудь поговорим, а пока об именах некоторых каталогов. Будем придерживаться традиций. Каталог с исполняемыми (двоичными или бинарными) программами обычно называют bin. Поскольку нам лучше отделить наш C-каталог от всего остального, то логично будет поместить его в c/bin. Полный путь в этом случае будет /home/username/c/bin. Неплохо иметь в каталоге c еще подкаталог doc для различных заметок. Все три каталога мы создадим одной командой:



Рис. 0-2. Выполнение команд в терминале.

Команда mkdir (MaKe DIRectory) создает каталог. С ключом -p (parents) создаются подкаталоги. Запись c/{bin,doc} равносильна записи c/bin c/doc. Диез # (он же решетка или хеш) создает комментарий в командной строке или командном файле. Команда оказывается довольно молчаливой. Система UNIX создавалась программистами для программистов и многие команды по умолчанию выполняются молча, без предупреждений. Предполагается, что пользователь хорошо знает, что делает. Вы тоже знаете, что делать, если не будете поначалу делать то, чего я не делаю.

Команда tree (TREE) показывает дерево подкаталогов относительно указанной точки. Здесь я забыл сказать одну важную вещь. Поскольку вы запускали терминал из меню своего рабочего стола, то ваш текущий каталог был /home/username. Всегда нужно помнить текущий каталог. Узнать его можно командой pwd (Print Work Directory). Перейти в любой каталог можно командой cd путь/в/каталог. Мнемоника команды cd - Change Directory. В качестве упражнения: перейдите в каталог c/bin и убедитесь, что вы находитесь там. Используйте относительные пути, нет необходимости указывать полный путь. Кстати, в вашем случае, наверняка будет подсказка в самом приглашении.

Еще немного о терминале. Когда-то терминал был реальным символьным устройством, подключенным к машине по линии последовательной связи или через модем. А еще раньше и вовсе использовался обычный рулонный телетайп, как в телеграфном отделении на почте. Сейчас это уже история. Тем не менее, текст в терминале прокручивается подобно бумаге в печатающем устройстве, а сам файл устройства так и называется tty (от teletype). Это не опечатка, в UNIX все является файлом, любое устройство, даже терминал. /dev/tty - это тот терминал, с которым вы сейчас работаете.

Но терминал это лишь “железо”, пусть и виртуальное. Что касается программ, то в терминале выполняется программа оболочки, shell. Оболочек несколько, но наиболее распространена оболочка bash (Bourne Again SHell, новая оболочка Борна). Почему оболочка? Вероятно потому, что внутри нее находится ядро, но не в буквальном смысле, а в смысле архитектуры. В действительности оболочка является обычной прикладной программой. Это командный процессор. Он передает команды пользователя, предварительно разобрав их, операционной системе для выполнения. Это единственный способ повлиять на ядро извне. Любая программа работает самостоятельно, но запустить ее можно только через оболочку, прямо или косвенно, через файл или исполняемый скрипт.

Давайте посмотрим, на месте ли программы, которые понадобятся нам для работы:



Рис. 0-3. Список нужных программ.

Программа cc - это компилятор языка C, as - ассемблер, ld - линкер. Это три программы, с которыми мы будем работать в самое ближайшее время. Немного о двух оставшихся. ar - это утилита архивации для библиотек, а make - менеджер проектов. Менеджеры проектов используют в сложных случаях, для нас это пока не актуально. Мы еще не скоро доберемся до использования make.

Нам понадобится и текстовый редактор. Это дело вкуса, редакторов много. Пожалуй, самый простой редактор - это команда UNIX cat. Давайте освоим ее прямо сейчас, тем более, что это совсем несложно. Заодно запишем на диск, в каталог c/bin важный для ближайшего будушего файл. Очистим экран сочетанием клавиш Ctrl-L и начнем писать:



Рис. 0-4. cat как редактор текста. С его помощью мы создаем нужный
для дальнейшего файл.

Оператор > здесь означает перенаправление вывода. Вообще-то, cat по умолчанию получает ввод из потока stdin а вывод направляет в поток stdout. Как stdin, так и stdout (и stderr) являются файлами. Они автоматически создаются при вызове любой программы. Поток stdin в нашем случае связан с клавиатурой, а stdout мы перенаправляем в дисковый файл crt0.s Таким образом, после ввода команды каждая написанная строчка (факт попадания текста на экран является всего лишь эхом) будет накапливаться в буфере, который затем будет записан в файл на диске.

Как редактор, cat совсем не подарок. Пока вы пишете строчку, вы можете ее редактировать обычным образом. Уже здесь заметны неудобства: так, например, попытка использовать для передвижения по строке клавиши со стрелками приводит к выводу неожиданных символов. Однако забой, backspce, действует исправно, что вполне логично для простого потока символов. Когда мы допишем последнюю строчку, то вводим управляющий символ EOF (end of file). Сделать это можно сочетанием клавиш Ctrl-D. (Повторный Ctrl-D в нашем случае закроет окно терминала.) cat хорош и удобен для быстрого создания маленьких файлов.

Для редактирования больших текстов, в UNIX, начиная с реликтового редактора ed, было создано со временем множество текстовых редакторов. Я предпочитаю и вам рекомендую редактор vi (vi, как и cc - ссылка на исполняемый файл, но об этом позже). На самом деле vi это vim (VI iMproved, улучшенный vi). Если его нет в системе, установите из репозитория, где он обязательно есть. Если вы когда-то работали с ed, то vi будет вам понятен с ходу. Это шутка, на самом деле я не навязываю вам выбор текстового редактора. Есть ne, nano, emacs, встроенный редактор mc (GNU Midnight Commander), и другие. Общего между ними то, то это - консольные редакторы, для терминала. Для X - графической оболочки - мы еще выберем редактор в свое время, а пока важно научиться работать в простом терминале. Это всегда может пригодиться.

Читатель уже начинает нервничать: “лучше бы я поставил MS Visual Studio или Borland C++ в любимую Windows, чем связаться с этим геморроем”. Сейчас я скажу еще одну ужасную вещь - у нас нет отладчика! Почему? А он не нужен.

Для тех, кто остался, я продолжаю. Попробуем открыть файл в редакторе vim. Просто наберем команду: vim crt0.s и посмотрим:



Рис. 0-5. Редактор vim и подсветка синтаксиса.

Редактор vi был создан задолго до появления мышей, поэтому он на них не реагирует. Современный vim можно настроить для использования мыши, но тогда пропадает смысл изучения редактора для терминала. Работа с vi довольно несложна, надо всего лишь запомнить несколько команд. Возможности этого редактора очень велики, но пока нам понадобится совсем немногое:

ESC - выход в режим команд, i - переход к вводу текста;
: - префикс команд (появится в последней строке терминала);
u - отмена последнего изменения;
w - запись на диск (выглядит как :w);
q - закрыть файл;
h - справка по командам и руководство.

ESC и i - это не команды, а скорее, переключатели режима. Команды вводятся после двоеточия, когда редактор находится в режиме команд.

Теперь у нас есть вся “среда разработки”. Советую немного поупражняться в редакторе vi (vim).

2. Библиотека времени выполнения - runtime library - RTL
В нашем случае, это тот самый файл crt0.s, который мы написали на языке ассемблера. Ассемблер для UNIX заметно отличается от ассмблера Intel, мы еще познакомимся с ним немного подробнее. Нужно сказать, что в этой книжке ассемблера у нас будет совсем немного, и нам придется, скорее, читать ассемблерный код, и крайне редко его писать.

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

Почему библиотека времени выполнения не написана на языке C? Потому, что здесь это гораздо проще сделать с помощью ассемблера. Нужна ли она вообще? Абсолютно, без нее мы не сможем запустить (вызвать) функцию main, в теле которой выполняется любая программа, написанная на языке C (не считая WinMain и LibMain для exe и dll Windows). Что означает crt0? Это означает C Run Time, а 0 - лишь то, что может быть1, 2 и т.д. На этом можно пока распрощаться с библиотекой времени выполнения.

Мы могли бы даже изменить имя функции main, но зачем? Нехорошо нарушать традиции.

3. Системные вызовы
Системный вызов (syscall) - неудачный перевод для названия системных функций. Функции - так лучше. Это посредники между ядром системы и приложениями. Иногда системные функции называют службами. Linux и UNIX имеют очень скромный набор системных функций, в сравнении с Windows. В Windows их насчитывается несколько тысяч, а в Linux - около 300. Для наших целей потребуется всего несколько. На первое время хватит всего трех: write, exit и read.

Мы уже говорили, что язык C не имеет никаких встроенных средств для ввода и вывода. Но мы можем организовать их самостоятельно. Для этого, нам придется написать еще несколько строк ассемблерного кода, который потом чудесным способом заработает в программах на C, не содержащих ни намека на ассемблер. Напишем сначала код write в новом файле write.s:



Рис. 0-6. Код функции write на ассемблере.

В самой первой строке нет опечатки. Именно write.c, а почему, мы узнаем потом.
Инструкция (или команда) movq $1, %rax помещает в регистр аккумулятора %rax число 1. Это номер системной функции write. Инструкция syscall обращается к ядру и оно читает номер переданной функции из регистра %rax. Все остальное делает уже код ядра. Инструкция ret возвращает значение из %rax в вызывающую функцию. Какое именно значение будет помещено в регистр %rax, зависит от функции. Что оно означает, мы выясним позже.

4. Главная функция main
В главной функции выполняется весь код программы, не считая кода библиотеки времени выполнения. Здесь мы переходим к языку C и напишем на нем нашу первую программу. Обычно такая программа называется hello, это тоже традиция. Откроем новый файл vi hello.c и запишем в него текст программы:



Рис. 0-7. Программа на языке C.

Если использовать команду ls (LiSt - показать список файлов каталога) в c/bin, то теперь здесь есть три файла: crt0.s, write.s, и hello.c. Это пример проекта (как бы громко это не было сказано) состоящего только из исходников. Теперь все это надо заставить работать. Это первая цель в нашем проекте. Вторая - показать на экране ожидаемый результат. Разбор всевозможных деталей самого языка можно отложить до решения технических проблем. Эти проблемы называются компиляцией.

5. Компиляция
Компилятор языка C переводит текст программы на язык ассемблера. Это его основная задача. Но обычно, компиляторы на этом не останавливаются и производят объектные модули. Это уже машинная программа, почти готовая для выполнения. Чтобы операционная система могла ее запустить, необходима окончательная обработка объектного модуля линкером. Сложные программы составляют из модулей, именно поэтому находится работа для линкера, редактора связей или компоновщика. Например, задача разрешения ссылок, - вычисление адресов для глобальных объектов, это работа как раз для линкера.

Наш “рабочий поток”, workflow, на примере проекта hello, будет выглядеть так:



Рис. 0-8. Компиляция проекта hello.

Работа здесь организована примерно так же, как в заводском цехе из нескольких участков. Первая операция состоит в передаче исходной программы hello.c компилятору. Если он не обнаружил ошибок, он должен выдать на своем выходе файл hello.s с ассемблерным кодом. Кое что мы там подправим вручную, но только на этот раз, затем мы автоматизируем большую часть работы.

Вторая команда передает полученный ассемблерный файл hello.s ассемблеру. Опять-таки, если нет ошибок, ассемблер as произведет объектный модуль hello.o. Этот файл изображен конвертиком, чтобы подчеркнуть его нечитаемость в текстовом редакторе, - это двоичный файл с машинными командами. Третья и четвертая операции совершенно аналогичны второй. Разница в том, что модули crt0.o и write.o полученные однажды, мы будем использовать в дальнейшем без каких-либо переделок в них. Таким образом, можно говорить о библиотечных модулях. Однако, crt0.o - это библиотека времени выполнения (она показана открытым ящиком, чтобы подчеркнуть тот факт, что фактически она является контейнером для всей программы), а write.o - “зародыш” библиотеки статической компоновки, это совсем другая библиотека.

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

Осталось лишь последовательно разобраться с самими командами. Начнем с первой:

$ cc -S hello.c
Лст. 0-1. Команда компиляции с выводом в ассемблерный файл.

Ключ -S указывает компилятору остановить работу на этапе получения ассемблерного кода и записать его в файл (не делать ни объектного модуля, ни целой программы). На диске окажется файл hello.s Открыв его редактором, вы увидите довольно много всего. Я отредактировал его, убрав все лишнее, что нам совершенно не понадобится. (Компилятор вставляет все это из самых общих соображений, а мы имеем дело с очень частным случаем.) Вот что нам из всего этого будет нужно:



Рис. 0-9. Модуль hello.s

Здесь есть о чем поговорить, но пока не будем отвлекаться от темы этого раздела. Выполним операции 2, 3 и 4. На этот раз нам нужен ассемблер, мы собираемся получить объектные модули файлов hello.s, crt0.s и write.s. Обрабатывать их можно в любом порядке, конечный результат от этого не зависит.

Для ассемблирования или трансляции в объектные модули понадобится команда:

$ as hello.s -o hello.o
Лст. 0-2. Вызов ассемблера.

Аналогичные команды следует выдать для остальных двух файлов: crt0.s и write.s. Если в исходных файлах нет синтаксических ошибок, то ассемблер молча выполнит команды.
Наконец, собираем объектные модули в работающую программу. Для этого вызываем линкер ld и передаем ему необходимые файлы:

$ ld -s hello.o crt.o write.o -o hello
Лст. 0-3. Связывание объектных модулей.

Все готово. Давайте посмотрим на наш рабочий каталог, и заодно на полезную команду du (Disk Used). В отличие от ls, тут мы можем быстро оценить размер всех файлов в байтах:



Рис. 0-10. Список файлов в рабочем каталоге.

Размер программы всего 424 байта.

6. Запуск hello
Мы добрались до готовности к запуску программы. Но, прежде чем выполнить ее, и чтобы не уродовать вызов лишними указаниями на текущий каталог, добавим к переменной окружения PATH путь к нашему рабочему каталогу. Это действует только в текущем сеансе работы с терминалом.

$ PATH=$PWD:$PATH
Лст. 0-3. Пример настройки переменных окружения.

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



Рис. 0-11. Работает.

Мы запустили традиционную программу Hello world, и, хотя пришли к этому результату не сразу, как в других книжках, но узнали заметно больше технических деталей. К тому же наша программа - одна из самых маленьких по размеру. Еще кто-нибудь верит, что “компилятор C создает огромные программы”?

И напоследок посмотрим, что вернулось из функции write в функцию main, а затем из main было передано операционной системе. bash сохраняет возвращенное значение в специальной переменной. Запустите hello и после выполнения программы наберите в командной строке echo $? а затем нажмите Enter. С чем вы можете связать этот результат?


Дальше

терминал, c, hello world!

Previous post Next post
Up