Добавлен семинар 1 по языкам программирования

This commit is contained in:
LeterZP
2026-09-30 00:09:23 +03:00
parent b4288cd2b1
commit 02f469090a
2 changed files with 221 additions and 1 deletions
@@ -0,0 +1,214 @@
## Содержание
- [[#Среда окружения]]
- [[#Использование интерактивной оболочки]]
- [[#Виды интерактивных оболочек]]
- [[#Стандартные потоки ввода-вывода]]
- [[#Дерево процессов]]
- [[#Регулярные выражения]]
- [[#Переменные окружения]]
- [[#Основы языка ассемблера]]
- [[#Компиляция ассемблерных программ]]
- [[#Простейшая программа]]
- [[#Секции, метки и комментарии]]
- [[#Директивы]]
- [[#Команды]]
- [[#Пошаговый анализ]]
- [[#Отладчик ассемблера gdb]]
- [[#Запуск программы]]
- [[#Основные команды]]
- [[#Форматы данных]]
## Среда окружения
### Использование интерактивной оболочки
Любая операционная система предполагает наличие интерфейса взаимодействия между пользователем и различными компонентами компьютерной системы. Чаще всего, таким интерфейсом является оболочка.
>**Оболочка** (англ. *shell*) - программный уровень, который поддерживает ввод команд и параметров для получения необходимых результатов.
Все оболочки имеют некоторые общие функции и свойства. Среди них интерпретация команд; доступ к командам и результатам их выполнения; обработка файлов, операция стандартного ввода и вывода; реализация специального языка программирования оболочки.
#### Виды интерактивных оболочек
Существует множество различных оболочек. Самой распространенной из всех является `bash` ([Bourne-Again Shell](https://ru.wikipedia.org/wiki/Bash)), которая используется по умолчанию для большинства дистрибутивов Linux. В MacOS по умолчанию стоит [zsh](https://ru.wikipedia.org/wiki/Zsh), а в Windows используется только [PowerShell](https://ru.wikipedia.org/wiki/PowerShell). Для того, чтобы узнать используемую оболочку, чаще всего достаточно посмотреть значение переменных `$SHELL` (в которой содержится путь до используемой по умолчанию в системе оболочки) или `$0` (в которой содержится имя текущего исполняемого процесса, а в контексте оболочки это как раз она).
Для того, чтобы изменить используемую оболочку, чаще всего достаточно команды `chsh`, которая изменяет оболочку пользователя. Использовать ее удобнее всего с флагом `-s/--shell` и путем к необходимой оболочке, которые сразу поставят необходимую оболочку. Важно помнить, что `chsh` перед установкой оболочки сверяется с содержимым файла `/etc/shells`, поэтому путь к необходимой оболочке должен быть в этом файле указан.
Основная суть оболочки `sh` ([Bourne shell](https://ru.wikipedia.org/wiki/Командная_оболочка_Unix)) в том, что она является стандартом POSIX-синтаксиса и имеет в себе только необходимые команды и функции для использования. Для повседневной работы `sh` не удобна, потому что в ней отсутствует множество в основном привычных и почти необходимых функций, и чаще всего она уступает более гибким вариантам оболочек. Тем не менее, `sh` удобно использовать для скриптов, которые не зависят от оболочки, ведь так как `sh` является стандартом, большинство оболочек имеют те же команды и могут исполнять скрипты `sh` по умолчанию. И именно из-за этого в настоящее время `sh` используется чаще всего не как интерактивная оболочка, а как просто интерпретатор для скриптов и автоматизации.
#### Стандартные потоки ввода-вывода
Для того, чтобы работать с пользовательским вводом и выводом, в оболочках предусмотрены потоки ввода-вывода. По умолчанию, стандартный поток ввода читается напрямую с клавиатуры, а стандартные потоки вывода пишутся в терминал. Для обозначения стандартного потока ввода используют `stdin` (с дескриптором 0), для обозначения стандартного потока вывода - `stdout` (с дескриптором 1), а для обозначения стандартного потока вывода ошибок - `stderr` (с дескриптором 2).
Для работы с потоками ввода-вывода в оболочке предусмотрены специальные символы перенаправления. Для перенаправления стандартного потока вывода в файл используются операторы `>` или `>>` (в зависимости от того, нужно ли нам перезаписывать файл, как это делает `>`, или дописать в конец файла, как это делает `>>`). Таким же образом можно перенаправить стандартный поток вывода в файл, только при этом явно указывая номер потока перед оператором (то есть, используя команды `2>` и `2>>`). Помимо файла, поток можно перенаправить и в другой поток теми же символами, указав после оператора `&` с номером потока (то есть, к примеру, `2>&1` направит вывод стандартного потока вывода ошибок в обычный стандартный поток вывода). Для стандартного потока ввода тоже есть похожие команды `<` и `<<`, но они работают немного по-другому. Оператор `<` читает содержимое указанного после него файла и отправляет в стандартный поток ввода. Оператор `<<` читает следующий вводимый текст до определенного символа, указанного после данного оператора, и также отправляет в стандартный поток ввода.
Помимо этого, с помощью стандартных потоков ввода-вывода можно перенаправлять результат одной команды в другую. Для этого используется конвейер `|`, который берет стандартный поток вывода первой команды и сразу перенаправляет его в стандартный поток ввода следующей в цепи команды.
#### Дерево процессов
>**Дерево процессов** - логическое представление всех процессов в операционной системе, связанных отношением "родитель - потомок".
В Linux все процессы, кроме самого первого, имеют своего родителя - процесс, который их создал. Это обеспечивает иерархию между процессами и помогает определить, какие процессы связаны и зависят друг от друга.
При этом у каждого процесса есть свой уникальный номер - `PID`, а также уникальный номер родителя - `PPID`. Это помогает следить за иерархией прямо внутри процессов. У корня дерева - основного процесса, у которого нет родителя - чаще всего устанавливается `PID 1`.
Очевидно, дерево процессов динамическое, потому что процессы в системе постоянно появляются и исчезают. Потоки внутри определенного процесса, по факту, тоже являются своего рода процессами, но их обычно к таковым не относят.
### Регулярные выражения
>**Регулярные выражения** - набор символов и/или метасимволов (символов, которые имеют особое значение), которые наделены особыми свойствами.
В данном случае мы будем рассматривать только регулярные выражения, используемые командой `grep`. Данная команда используется для поиска данных по заданному шаблону. Она может найти подходящие под шаблон строки, слова, количество таких строк или слов и много чего еще.
При задании шаблона в `grep` чаще всего используются специальные символы. Ниже представлены основные из них.
- `*` - символ слева может повторяться ноль или более раз
- `\+` - символ слева может повторяться один или более раз
- `?` - символ слева может повторятся ноль или один раз
- `\{` и `\}` - число точного количества повторений символа слева
- `.` - любой символ
- `^` - начало строки
- `$` - конец строки
- `[` и `]` - подмножество допустимых символов (символ `^` внутри квадратных скобок используется для отрицания)
- `\<` и `\>` - начало и конец слова
- `\` - экранирование специальных символов
Больше информации про `grep` и регулярные выражения можно найти в [статье](https://www.opennet.ru/docs/RUS/bash_scripting_guide/c11895.html).
### Переменные окружения
>**Переменные окружения** - именованные переменные, содержащие текстовую информацию, которую могут использовать запускаемые программы.
В основном, такие переменные содержат общие настройки системы, параметры графической или командной оболочки, данные о предпочтениях пользователя и многое другое.
Некоторые переменные окружения хранят значения, которые используются для особых целей.
- `LD_LIBRARY_PATH` - директория, где по умолчанию ищутся динамические библиотеки
- `PATH` - место размещения исполняемых файлов в системе
- `EDITOR` - имя предпочитаемого текстового редактора
При создании дочернего процесса от родительского по умолчанию наследуются все переменные окружения. Но данные переменные могут быть явно изменены во время или после запуска дочернего процесса. Переменные могут изменится во время создания процесса, либо если явно указать передаваемые переменные или заменить их, либо если система сочтет небезопасным передавать эти переменные окружения новому процессу. Также переменные могут быть изменены уже внутри дочернего процесса. При этом изменение переменных в дочернем процессе никак не могут повлиять на переменные в родительском процессе.
Помимо переменных окружения существуют также и переменные оболочки. В отличие от переменных окружения, они живут только внутри одного процесса и не передаются ни вверх по [[#Дерево процессов|дереву процессов]], ни вниз. Для того, чтобы из переменной оболочки сделать переменную окружения, которая могла бы передаваться в другие процессы, используется ключевое слово `export`.
## Основы языка ассемблера
### Компиляция ассемблерных программ
Сам файл с языком ассемблера имеет расширение `.asm`. Компиляция такого файла в исполняемый машинный код происходит в два этапа. На первом этапе используется компилятор, который создает из файла с расширением `.asm` файл с расширением `.o` - файл, содержащий все машинные инструкции вместо кода, но вместо настоящих операндов-адресов памяти имеющий заглушки. На втором этапе линкер (или компоновщик) создает из одного или более объектных файлов с расширением `.o` один исполняемый файл без расширения.
Для компиляции на первом этапе используется команда `nasm` (Netwide Assembler). В [[#Простейшая программа|простейшей программе]] далее при компиляции используются флаги `-g`, `-felf64` и `-o`. Флаг `-g` выводит debug-информацию о процессе компиляции. Флаг `-f` устанавливает формат итогового файла, в данном случае `elf64`, что является стандартным вариантом для большинства Unix-подобных систем на архитектуре `x86-64`. Флаг `-o` указывает, в какой файл записать вывод программы.
Для компоновки на втором этапе используется команда ld (стандартный линкер для GNU). В [[#Простейшая программа|простейшем примере]] далее при компиляции используется флаг `-o`. Как и в описании флагов для компилятора, данный флаг записывает результат работы программы в указанный файл.
### Простейшая программа
Далее подробно рассмотрим простейшую программу на языке ассемблера, которая выводит "hello, world!" в стандартный поток вывода.
```
; hello.asm
section .data
message: db 'hello, world!', 10
section .text
global _start
_start:
mov rax, 1 ; 'write' syscall number
mov rdi, 1 ; stdout descriptor
mov rsi, message ; string address
mov rdx, 14 ; string length in bytes
syscall
mov rax, 60 ; 'exit' syscall number
xor rdi, rdi
syscall
```
#### Секции, метки и комментарии
Как известно из [архитектуры фон-Неймана](https://ru.wikipedia.org/wiki/Архитектура_фон_Неймана), программа хранит как данные, так и коды инструкции в одном месте - нераздельной памяти. Но для удобства использования программы их все же необходимо разделить. Поэтому программы на языке ассемблера чаще всего делят на секции.
>**Секция** - специальный метод деления кода на части, призванный разделить различные логически раздельные части друг от друга.
Для каждой отдельной секции существует свой способ и место применения. Так, в простейшей программе используются секции `.text` для объявления инструкций, которые должна выполнять программа, и `.data` для объявления глобальных переменных (переменных, доступных во время всего исполнения программы). При этом даже если в коде программы постоянно чередовать одни и те же секции, при компиляции одинаковые секции все равно объединяются вместе и в память соответственно будут записываться вместе.
Для того, чтобы не использовать прямые адреса к значениям (что достаточно трудно и доставляет много проблем), используются `labels` (метки). По сути, это просто читаемые названия адресов. В примере программы используется только одна метка - `_start`.
Также стоит заметить, что в отличие от всего остального кода названия секций и меток чувствительны к регистру. Иными словами, инструкции в программе могут быть записаны в любом регистре, и компилятор поймет это, но метки или секции с разным регистром уже будут расцениваться как две разные метки или секции.
Одна программа может делится на несколько файлов и запускаться вся сразу. Для того, чтобы не запутаться в порядке исполнения программы и запустить именно то, что нужно, существует точка входа в программу. В случае языка ассемблера точкой входа обычно служит метка `_start` - это часть программы, которая при запуске отрабатывает первее всех. При этом важно, что метка с таким названием обязательно должна быть указана [[#Директивы|директивой]] `global`.
Также в языке ассемблера можно писать комментарии к коду. Для этого используется специальный символ `;`, после которого до конца строки текст не будет расцениваться как код программы.
#### Директивы
В основном, язык ассемблера состоит из команд, которые при компиляции превращаются напрямую в машинные команды. Но не все инструкции в языке являются командами.
>**Директивы** - конструкции в языке, которые являются не командами для исполнения, а командами, контролирующими процесс перевода программы в машинный код. Иногда некоторые директивы также называют псевдо инструкциями.
В простейшем примере присутствуют три директивы: команды `global`, `section` и `db`. Секции были рассмотрены ранее, а `global` будет рассмотрен позже. Директива `db` используется для создания байтовых данных (`db = data bytes`).
Также существуют другие директивы, использующиеся для определения данных. Чаще всего из них используются `dw` (`dw = data word`, двухбайтовое слово), `dd` (`dd = data double`, четырехбайтовое удвоенное слово) и `dq` (`dq = data quad`, восьмибайтовое учетверенное слово). Все эти директивы кодируют входящие символы в коды ASCII. В каждой из указанных ранее директив для хранения данных можно указывать несколько значений подряд через запятую. Так, в примере после самой строки на вывод указывается через запятую символ с кодом 10, который завершает строку.
Помимо этого, существует полезная директива `times`, которая позволяет повторять указанную после нее команду заданное количество раз.
#### Команды
Команда `xor` выполняет операцию логического "или" между двумя указанными регистрами и записывает результат в первый из указанных. Очень часто данная команда используется для обнуления указанного дважды регистра.
Команда `mov` используется для того, чтобы записать значение из второго операнда в первый. Операнды могут быть регистром или памятью, а второй - еще и явным значением. Тем не менее, важно учитывать два важных недостатка: эта команда не может копировать данные из памяти в память, а также источник и место назначения должны быть одного и того же размера.
Команда `syscall` позволяет исполнять системные вызовы внутри программы. Из-за того, что операции ввода-вывода зависят от устройства, а также могут использоваться несколькими программами одновременно, в коде нельзя контролировать эти операции напрямую, в обход операционной системы. Каждый системный вызов имеет собственный номер. При исполнении вызова программа берет его номер из регистра `rax`, а необходимые аргументы (в зависимости от их количества последовательно) из регистров `rdi`, `rsi`, `rdx`, `r10`, `r8` и `r9`. Соответственно, системный вызов не может принимать больше шести аргументов. При исполнении команда меняет значения в регистрах `rcx` и `r11`.
Важно помнить, что программа не завершится, пока данная инструкция не будет указана явно. Иными словами, если убрать из примера последние три строки с вызовом завершения программы, то она продолжит работать, читая из памяти последующие значения. При этом эти значения, если они не были явно обработаны памятью, никак не могут быть нулями или правильными инструкциями, поэтому программа завершится с ошибкой с кодом 6 (инвалидные инструкции).
#### Пошаговый анализ
Таким образом, можно рассмотреть программу по шагам.
1. Запись переменной `message` с данными `hello world!` и окончанием строки с помощью команды `db` в секции `.data`
2. Начало секции `.text` и объявление метки `_start` в область `global`
3. Начало программы с адресом в `_start`
4. Установка номера системного вызова записи `write` 1 в регистр `rax`
5. Установка первого аргумента системного вызова, в данном случае номер дескриптора системного потока вывода 1, в регистр `rdi`
6. Установка второго аргумента системного вызова, в данном случае адрес начала читаемой строки `message`, в регистр `rsi`
7. Установка третьего аргумента системного вызова, в данном случае количество байтов для чтения 14, в регистр `rdx`
8. Системный вызов
9. Установка номера системного вызова завершения программы `exit` 60 в регистр `rax`
10. Установка первого аргумента системного вызова в ноль
11. Системный вызов
Во время исполнения программы можно увидеть постоянные изменения регистра `rip`. Он указывает на адрес следующей исполняемой инструкции программы.
После выполнения системного вызова в регистр `rax` записывается результат выполнения системного вызова. Для системного вызова `write` в `rax` записывается число реально записанных байт или отрицательный код ошибки.
### Отладчик ассемблера gdb
>**Отладчик** - инструмент, который позволяет запускать программу под своим контролем, останавливать ее в нужных местах и изучать ее состояние: значения переменных, память, стек вызовов, потоки, файлы, регистры процессора.
gdb - популярный и мощный отладчик для языков ассемблера и C. В его основные функции входит исполнение программы по шагам, изучение состояния программы и слепка регистров и памяти на каждом шаге, а также управление отладчиком с помощью специальной системы команд. Помимо этого он также может перемещаться и изучать стек вызовов, вычислять арифметические выражения, запускать функции, автоматизировать отладку с помощью скриптов на Python и многое другое.
В коде можно установить специальные точки останова (англ. *breakpoint*), которые будут останавливать программу в нужном месте.
У gdb есть специальная [[#Основные команды|система команд]], с помощью которой можно управлять процессом отладки. Поэтому для этого в него встроена командная строка. Помимо этого, с gdb можно работать и через интерактивный интерфейс. Но с помощью команд можно создавать скрипты и автоматизировать отладку программы, что иногда бывает удобнее.
#### Запуск программы
Для запуска программы в отладчике gdb есть два способа: используя встроенную в gdb команду `file`, или запустив отладчик сразу с именем программы в качестве аргумента.
С помощью сочетания клавиш `Ctrl-X` и `o` можно менять фокус на окнах и взаимодействовать с выбранным окном. С помощью сочетания клавиш `Ctrl-L` можно перерисовать интерфейс отладчика.
#### Основные команды
Две самые важные команды в отладчике - это команды `quit` и `help`.
В число основных команд также входят.
- `run` или `r` - запуск выполнения программы
- `break <метка>` или `b <метка>` - установка точки останова в программе по имени метки
- `break *<адрес>` или `b *<адрес>` - установка точки останова в программе на указанный адрес
- `continue` - продолжение выполнения программы
- `stepi` или `si` - выполнение ровно одной инструкции
- `nexti` или `ni` - выполнение ровно одной инструкции, при этом не заходя в функции, вызванные при помощь команды `call`, а выполняя всю функцию за шаг
- `layout` - переключение режима интерфейса
- `print /<format> <val>` - просмотр содержимого регистра или памяти; в `<format>` указывается формат данных (необязательно); в `<val>` указывается метка адреса памяти или знак `$` с именем регистра
- `x /<format> <address>` - аналог `print`, но принимает адрес напрямую (или регистр); можно использовать символ `&` для взятия адреса с метки, что позволяет этой команде также работать с метками
- `record` - запуск записи
- `record stop` - завершение записи
- `reverse-stepi` или `rsi` - шаг назад ровно на одну инструкцию (обратная к `si`)
- `reverse-nexti` или `rni` - шаг назад ровно на одну инструкцию, но без захода в функции (обратная к `ni`)
Чаще всего с командой `layout` используется аргумент `regs`, но иногда можно и `asm`.
#### Форматы данных
В некоторых командах можно указывать форматы выходных данных, что иногда удобно при отладке.
- `x` - hexadecimal - шестнадцатеричная система счисления
- `a` - address - адрес
- `i` - instruction - инструкция (gdb попытается машинный код превратить в команду ассемблера)
- `c` - char - символ
- `s` - null-terminated string - строка с символами ASCII и с окончанием символом `0x00`
- `b` - byte - байт
- `g` - giant - 8 байт
- любое число - количество выбираемых объектов