Repository: victor-yacovlev/mipt-diht-caos Branch: master Commit: ff7b8d86ff9a Files: 207 Total size: 690.1 KB Directory structure: gitextract_ddi_ocjg/ ├── .gitignore ├── 2018-2019.md ├── 2019-2020-informatics.md ├── 2019-2020-physics.md ├── 2019-2020.md ├── 2020-2021-informatics.md ├── 2020-2021-os-course.md ├── 2020-2021.md ├── 2021-2022-full-year-course.md ├── 2021-2022-half-year-course.md ├── 2021-2022.md ├── 2022-2023-full-year-course.md ├── 2022-2023.md ├── LICENSE ├── en-mipt/ │ ├── Exam-Fall.md │ ├── README.md │ ├── admin-basics/ │ │ └── README.md │ ├── arm/ │ │ └── arm.md │ ├── dev-tools/ │ │ ├── dev-tools.md │ │ └── my-first-program.c │ ├── fds/ │ │ └── README.md │ ├── linux-basics/ │ │ └── linux-intro.md │ ├── numbers/ │ │ └── README.md │ ├── slides/ │ │ └── presentation-sources/ │ │ ├── 01-Introduction.odp │ │ ├── 02-LinuxBasics.odp │ │ ├── 03-DevelopmentBasics.odp │ │ ├── 04-NumbersRepresentation.odp │ │ ├── 06-x86-Interrupts-Syscalls.prdx │ │ └── 07-Kernel-FIlesAPI.prdx │ └── syscalls/ │ └── README.md ├── harbour/ │ ├── README.md │ ├── arm/ │ │ ├── arm.md │ │ └── memory_addressing.md │ ├── asm-x86/ │ │ └── README.md │ ├── files/ │ │ └── README.md │ ├── ieee754/ │ │ └── README.md │ ├── ints/ │ │ └── README.md │ ├── libs/ │ │ └── README.md │ ├── mmap/ │ │ └── README.md │ ├── openssl/ │ │ └── README.md │ ├── pipes/ │ │ └── README.md │ ├── signals/ │ │ └── README.md │ ├── slides/ │ │ └── 02_Data representation.pptx │ ├── sockets/ │ │ └── README.md │ └── time/ │ └── README.md ├── lectures/ │ ├── fall-2018/ │ │ └── Lection07-supplementary-01.c │ ├── fall-2019/ │ │ ├── Supplementary-06/ │ │ │ ├── lib_and_exec_demo/ │ │ │ │ ├── Makefile │ │ │ │ ├── file.c │ │ │ │ └── test.py │ │ │ ├── rpath_demo/ │ │ │ │ ├── Makefile │ │ │ │ └── src/ │ │ │ │ ├── mygreatlib.c │ │ │ │ ├── mygreatlib.h │ │ │ │ └── program.c │ │ │ └── toyos/ │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── boot.s │ │ │ ├── grub.cfg │ │ │ ├── kernel.c │ │ │ └── linker.ld │ │ ├── Supplementary-08/ │ │ │ └── custom-fd.c │ │ ├── Supplementary-10/ │ │ │ ├── memory-map.c │ │ │ ├── overcommit.c │ │ │ ├── test-malloc.c │ │ │ └── test-malloc2.c │ │ ├── Supplementary-11/ │ │ │ ├── fork-bomb.c │ │ │ ├── process_setup.c │ │ │ └── start_child.c │ │ ├── Supplementary-12/ │ │ │ ├── do_abort.c │ │ │ ├── good-signal-handling.c │ │ │ ├── handle-sigint-sigterm.c │ │ │ ├── sigaction-handling.c │ │ │ ├── signalfd.c │ │ │ ├── sigprocmask.c │ │ │ ├── sigsuspend.c │ │ │ └── simpleio.c │ │ └── Supplementary-13/ │ │ ├── ldpreload-example/ │ │ │ ├── fakelib.c │ │ │ ├── fakelib0.c │ │ │ ├── fakelib1.c │ │ │ ├── hello.c │ │ │ └── solution.c │ │ ├── ptrace/ │ │ │ └── ptrace_catch_string.c │ │ └── wrap-example/ │ │ ├── fakelib.c │ │ └── solution.c │ ├── spring-2019/ │ │ ├── Lection14-Supplementary/ │ │ │ ├── do_abort.c │ │ │ ├── do_nothing.c │ │ │ ├── good-signal-handling.c │ │ │ ├── handle-sigint-sigterm.c │ │ │ └── sigaction-handling.c │ │ ├── Lection15-Supplementary/ │ │ │ ├── signalfd.c │ │ │ ├── sigprocmask.c │ │ │ └── sigsuspend.c │ │ ├── Lection18-Supplementary/ │ │ │ └── lorem-ipsum-server.cpp │ │ └── Lection20-Supplementaty/ │ │ └── detached-threads.c │ └── spring-2020/ │ └── Lection17-Supplementary/ │ └── detached-threads.c ├── lessons-supplementary/ │ └── 2021-2022/ │ ├── l18-shm/ │ │ └── shm.c │ ├── l19-bpf/ │ │ ├── example1.c │ │ ├── example2.c │ │ └── filter.s │ ├── l20-ebpf/ │ │ ├── bpf_loader.c │ │ ├── bpf_program.c │ │ ├── call_some_func.c │ │ ├── trace_call_time.c │ │ ├── trace_some_func.c │ │ ├── trace_syscall.c │ │ └── trace_syscall_1.c │ ├── l21-libraries/ │ │ ├── ctor_dtor/ │ │ │ ├── module.c │ │ │ └── run_lib.c │ │ ├── export_func_by_name/ │ │ │ └── main.c │ │ ├── runnable_lib/ │ │ │ └── main.c │ │ ├── use_dlopen/ │ │ │ └── run_function.c │ │ ├── use_mmap/ │ │ │ ├── plugin.c │ │ │ └── run.c │ │ └── use_rpath/ │ │ ├── library.c │ │ └── program.c │ ├── l23-grpc/ │ │ ├── CMakeLists.txt │ │ ├── cplusplus/ │ │ │ ├── profile_server_main.cpp │ │ │ ├── profile_service.cpp │ │ │ └── profile_service.h │ │ ├── dart/ │ │ │ ├── .gitignore │ │ │ ├── .metadata │ │ │ ├── README.md │ │ │ ├── analysis_options.yaml │ │ │ ├── lib/ │ │ │ │ ├── main.dart │ │ │ │ └── src/ │ │ │ │ ├── generated/ │ │ │ │ │ ├── social_network.pb.dart │ │ │ │ │ ├── social_network.pbenum.dart │ │ │ │ │ ├── social_network.pbgrpc.dart │ │ │ │ │ └── social_network.pbjson.dart │ │ │ │ └── main_screen.dart │ │ │ ├── pubspec.yaml │ │ │ └── web/ │ │ │ ├── index.html │ │ │ └── manifest.json │ │ ├── go/ │ │ │ ├── chat_server_main.go │ │ │ ├── chat_service.go │ │ │ └── go.mod │ │ ├── python/ │ │ │ ├── main.py │ │ │ ├── social_network_pb2.py │ │ │ └── social_network_pb2_grpc.py │ │ └── social_network.proto │ └── l24-kernel-fs/ │ ├── fuse/ │ │ └── fusepy-memory-example.py │ └── modules/ │ ├── Makefile │ ├── hello-with-param.c │ ├── hello.c │ └── hello2.c ├── practice/ │ ├── .clang-format │ ├── aarch64/ │ │ └── README.md │ ├── aarch64-functions/ │ │ └── README.md │ ├── arm/ │ │ └── README.md │ ├── arm_globals_plt/ │ │ └── README.md │ ├── asm/ │ │ ├── arm_basics/ │ │ │ └── README.md │ │ ├── arm_load_store/ │ │ │ └── README.md │ │ ├── nostdlib_baremetal/ │ │ │ ├── README.md │ │ │ └── toyos/ │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── boot.s │ │ │ ├── grub.cfg │ │ │ ├── kernel.c │ │ │ └── linker.ld │ │ ├── x86_basics/ │ │ │ └── README.md │ │ └── x86_fpmath/ │ │ └── README.md │ ├── bash-grep-sed/ │ │ └── README.md │ ├── bpf/ │ │ └── README.md │ ├── codestyle.md │ ├── epoll/ │ │ └── README.md │ ├── exec-rlimit-ptrace/ │ │ ├── README.md │ │ ├── get_limits.c │ │ ├── ptrace_catch_string.c │ │ └── shell_with_custom_stack_size.c │ ├── fdup-pipe/ │ │ └── README.md │ ├── file_io/ │ │ └── README.md │ ├── fork/ │ │ └── README.md │ ├── function-pointers/ │ │ ├── README.md │ │ ├── dynload.c │ │ ├── func-pointer.c │ │ ├── lib.c │ │ └── main.c │ ├── fuse/ │ │ └── README.md │ ├── http-curl/ │ │ └── README.md │ ├── ieee754/ │ │ └── README.md │ ├── integers/ │ │ └── README.md │ ├── linux_basics/ │ │ ├── README.md │ │ ├── cmake.md │ │ ├── devtools.md │ │ ├── intro.md │ │ └── my-first-program.c │ ├── math/ │ │ └── README.md │ ├── mmap/ │ │ └── README.md │ ├── mutex-condvar-atomic/ │ │ └── README.md │ ├── openssl/ │ │ └── README.md │ ├── posix_dirent_time/ │ │ └── README.md │ ├── posix_ipc/ │ │ └── README.md │ ├── pthread/ │ │ └── README.md │ ├── python/ │ │ └── README.md │ ├── signal-1/ │ │ └── README.md │ ├── signal-2/ │ │ ├── README.md │ │ └── sigprocmask.c │ ├── sockets-tcp/ │ │ └── README.md │ ├── sockets-udp/ │ │ └── README.md │ ├── stat_fcntl/ │ │ └── README.md │ ├── time/ │ │ └── README.md │ └── x86-64/ │ └── README.md └── projects/ ├── README.md ├── assembler_macroces.md ├── compiler.md ├── httpd.md ├── proxy.md ├── shell.tex └── task_doom.tex ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .vscode/settings.json ================================================ FILE: 2018-2019.md ================================================ # АКОС - архив Группы 793, 794, 795, 796, 797, 798 и 7910 ФИВТ МФТИ. * [Coding Standards для используемого в курсе языка Си](practice/codestyle.md) * [Семестровый проект](projects/) ## Лекции ### Презентации лекций в формате PDF * [Осенний семестр 2018](lectures/fall-2018) * [Весенний семестр 2018](lectures/spring-2019) ### Видео лекций * Записываются силами студсовета и публикуются на [YouTube](https://www.youtube.com/playlist?list=PL4_hYwCyhAva4dDOnyddyvkAs_jWVr624) ## Семинарские занятия 1. [Введение в Linux и инструменты разработки](practice/linux_basics/) 2. [Целочисленная арифметика](practice/integers/) 3. [Часть 1: Инструменты для ARM](practice/arm/) [Часть 2: Ассемблер ARM](practice/asm/arm_basics/) 4. [Адресация памяти](practice/asm/arm_load_store/) 5. [Глобальные переменные, константы и библиотечные функции Си](practice/asm/arm_globals_plt/) 6. [Ассемблер x86](practice/asm/x86_basics/) 7. [Вещественные операции и SSE](practice/asm/x86_fpmath/) 8. [Системные вызовы](practice/asm/nostdlib_baremetal/) 9. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 10. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 11. [POSIX API для работы с файловой системой и временем](practice/posix_dirent_time/) 12. [Отображение файлов на память](practice/mmap/) 13. [Запуск и завершение работы процессов](practice/fork/) 14. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) 15. [Указатели на функции и динамические библиотеки](practice/function-pointers/) 16. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 17. [Сигналы. Часть 1](practice/signal-1/) 18. [Сигналы. Часть 2](practice/signal-2/) 19. [Разделяемая память и семафоры POSIX](practice/posix_ipc/) 20. [Сокеты TCP/IP](practice/sockets-tcp/) 21. [Сокеты UDP](practice/sockets-udp/) 22. [Мультиплексирование ввода-вывода](practice/epoll/) 23. [Многопоточность POSIX Threads](practice/pthread/) 24. [Многопоточная синхронизация](practice/mutex-condvar-atomic/) 25. [Протокол HTTP/1.1. Сборка с помощью CMake](practice/http-cmake/) 26. [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) ================================================ FILE: 2019-2020-informatics.md ================================================ # АКОС на ПМИ+ИВТ ### Осень 2019 1. [Введение в Linux](practice/linux_basics/intro.md) 2. [Инструменты разработки в UNIX](practice/linux_basics/devtools.md) 3. [Часть 1: Целочисленная арифметика](practice/integers/) [Часть 2: Вещественная арифметика](practice/ieee754/) 4. [Часть 1: Инструменты для ARM](practice/arm/) [Часть 2: Ассемблер ARM](practice/asm/arm_basics/) 5. [Адресация данных в памяти и использование библиотечных функций](practice/arm_globals_plt/) 6. [Ассемблер x86](practice/asm/x86_basics/) 7. [Вещественные операции и SSE](practice/asm/x86_fpmath/) 8. [Системные вызовы](practice/asm/nostdlib_baremetal/) 9. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 10. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 11. [Отображение файлов на память](practice/mmap/) 12. [Запуск и завершение работы процессов](practice/fork/) 13. [Сигналы. Часть 1](practice/signal-1/) 14. [Сигналы. Часть 2](practice/signal-2/) 15. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) ### Весна 2020 1. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 2. [Сокеты TCP/IP](practice/sockets-tcp/) 3. [Мультиплексирование ввода-вывода](practice/epoll/) 4. [Многопоточность POSIX Threads](practice/pthread/) 5. [Многопоточная синхронизация](practice/mutex-condvar-atomic/) 6. [Разделяемая память и семафоры POSIX](practice/posix_ipc/) 7. [Указатели на функции и динамические библиотеки](practice/function-pointers/) 8. [Сокеты UDP и AF_PACKET](practice/sockets-udp/) 9. [Berkley Packet Filter](practice/bpf/) 10. Неделя с 6 по 11 апреля: [Часть 1: Протокол HTTP/1.1 и cURL](practice/http-curl/) [Часть 2: Сборка с помощью CMake](practice/linux_basics/cmake.md) 11. Неделя с 13 по 18 апреля: [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) 12. Неделя с 20 по 25 апреля: [Часть 1: Работа с каталогами в POSIX](practice/posix_dirent_time). [Часть 2: Файловые системы FUSE](practice/fuse/) 13. Неделя с 27 апреля по 9 мая: **новых тем не будет, только прием задач**. Дополнительный семинар: [Время в UNIX](practice/time/) 14. Неделя с 11 по 16 мая: [Python Extending and Embedding](practice/python/) ================================================ FILE: 2019-2020-physics.md ================================================ # АКОС на ПМФ 1. [Введение в Linux и инструменты разработки](practice/linux_basics/) 2. [Часть 1: Целочисленная арифметика](practice/integers/) [Часть 2: Вещественная арифметика](practice/ieee754/) 3. [Часть 1: Инструменты для ARM](practice/arm/) [Часть 2: Ассемблер ARM](practice/asm/arm_basics/) [Часть 3: Адресация памяти в ARM-системах](practice/asm/arm_load_store/) 4. [Глобальные переменные, константы и библиотечные функции Си](practice/arm_globals_plt/) 5. [Часть 1. Ассемблер x86](practice/asm/x86_basics/) [Часть 2. Вещественные операции и SSE](practice/asm/x86_fpmath/) 6. [Системные вызовы](practice/asm/nostdlib_baremetal/) 7. [Часть 1. Низкоуровневый файловый ввод и вывод](practice/file_io/) [Часть 2. Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 8. [POSIX API для работы с файловой системой и временем](practice/posix_dirent_time/) 9. [Отображение файлов на память](practice/mmap/) 10. [Часть 1. Запуск и завершение работы процессов](practice/fork/) [Часть 2. Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) 11. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 12. [Сигналы. Часть 1](practice/signal-1/) [Сигналы. Часть 2](practice/signal-2/) 13. [Часть 1. Сокеты TCP/IP](practice/sockets-tcp/) [Часть 2. Сокеты UDP](practice/sockets-udp/) 14. [Мультиплексирование ввода-вывода](practice/epoll/) ================================================ FILE: 2019-2020.md ================================================ # АКОС - архив * [English Version for Harbour Space Joint Program](harbour/) * [English Version for Foreigners MIPT Program](en-mipt/) ------ * [2019-2020: направления ПМИ и ИВТ](2019-2020-informatics.md) * [2019-2020: направление ПМФ и матгруппа](2019-2020-physics.md) ================================================ FILE: 2020-2021-informatics.md ================================================ # АКОС на ПМИ ### Осень 2020 1. [Введение в Linux](practice/linux_basics/intro.md) 2. [Базовые инструменты разработки в UNIX](practice/linux_basics/devtools.md) 3. [Часть 1: Сборка с помощью CMake](practice/linux_basics/cmake.md) [Часть 2: Python Extending and Embedding](practice/python/) 4. [Часть 1: Целочисленная арифметика](practice/integers/) [Часть 2: Вещественная арифметика](practice/ieee754/) 5. [Часть 1: Инструменты для ARM](practice/arm/) [Часть 2: Ассемблер ARM](practice/asm/arm_basics/) 6. [Адресация данных в памяти и использование библиотечных функций](practice/arm_globals_plt/) 7. [Ассемблер x86](practice/asm/x86_basics/) 8. [Вещественные операции и SSE](practice/asm/x86_fpmath/) 9. [Системные вызовы](practice/asm/nostdlib_baremetal/) 10. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 11. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 12. [Отображение файлов на память](practice/mmap/) 13. [Запуск и завершение работы процессов](practice/fork/) 14. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) ### Весна 2021 1. [Сигналы. Часть 1](practice/signal-1/) 2. [Сигналы. Часть 2](practice/signal-2/) 3. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 4. [Сокеты TCP/IP](practice/sockets-tcp/) 5. [Мультиплексирование ввода-вывода](practice/epoll/) 6. [Многопоточность POSIX Threads](practice/pthread/) 7. [Мьютексы, условные переменные, атомарные переменные](practice/mutex-condvar-atomic/) 8. [Разделяемая память и семафоры POSIX](practice/posix_ipc/) 9. [Указатели на функции и динамические библиотеки](practice/function-pointers/) 10. [Сокеты UDP и AF_PACKET](practice/sockets-udp/) 11. [Berkley Packet Filter](practice/bpf/) 12. [Протокол HTTP/1.1 и cURL](practice/http-curl/) 13. [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) 14. [Часть 1: Работа с каталогами в POSIX](practice/posix_dirent_time). [Часть 2: Файловые системы FUSE](practice/fuse/) 15. [Время в UNIX](practice/time/) ================================================ FILE: 2020-2021-os-course.md ================================================ # Операционные системы на ИВТ ### Осень 2020 1. [Часть 1: Введение в Linux](practice/linux_basics/intro.md) [Часть 2: Базовые инструменты разработки в UNIX](practice/linux_basics/devtools.md) 2. [Системные вызовы](practice/asm/nostdlib_baremetal/) 3. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 4. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 5. [Отображение файлов на память](practice/mmap/) 6. [Запуск и завершение работы процессов](practice/fork/) 7. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) 8. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 9. [Сигналы. Часть 1](practice/signal-1/) [Сигналы. Часть 2](practice/signal-2/) 10. [Сокеты TCP/IP](practice/sockets-tcp/) 11. [Мультиплексирование ввода-вывода](practice/epoll/) 12. [Сокеты UDP и AF_PACKET](practice/sockets-udp/) [Berkley Packet Filter](practice/bpf/) 13. [Часть 1: Работа с каталогами в POSIX](practice/posix_dirent_time). [Часть 2: Файловые системы FUSE](practice/fuse/) 14. [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) 15. [Время в UNIX](practice/time/) ================================================ FILE: 2020-2021.md ================================================ # АКОС - Operating Systems Course ![Лицензия Creative Commons](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) Все материалы доступны по лицензии [Creative Commons «Attribution-ShareAlike» («Атрибуция-СохранениеУсловий») 4.0 Всемирная](http://creativecommons.org/licenses/by-sa/4.0/). [Coding Standards для используемого в курсе языка Си](practice/codestyle.md) [Конфиг для clang-format](practice/.clang-format) * [Направление ПМФ](2019-2020-physics.md) * [Направление ИВТ](2020-2021-os-course.md) * [Направление ПМИ](2020-2021-informatics.md) ---- * [Архив: 2019-2020 учебный год](2019-2020.md) * [Архив: 2018-2019 учебный год](2018-2019.md) ================================================ FILE: 2021-2022-full-year-course.md ================================================ # АКОС на ПМИ ### Модуль 1 (осень 2021) 1. [Введение в Linux и базовые инструменты разработки](practice/linux_basics/) 2. [Командный интерпретатор bash и утилита sed](practice/bash-grep-sed) 3. [Целочисленная и вещественная арифметика](practice/math) 4. [Архитектура AArch64 и язык ассемблера](practice/aarch64) 5. [Архитектура AArch64 - стек и вызов функций](practice/aarch64-functions) 6. [Архитектура x86-64 и язык ассемблера](practice/x86-64) ### Модуль 2 (осень 2021) 1. [Системные вызовы](practice/asm/nostdlib_baremetal/) 2. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 3. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 4. [Отображение файлов на память](practice/mmap/) 5. [Запуск и завершение работы процессов](practice/fork/) 6. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) ### Модуль 3 (весна 2022) 1. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 2. [Сигналы](practice/signals/) 3. [Сокеты TCP/IP](practice/sockets-tcp/) 4. [Мультиплексирование ввода-вывода](practice/epoll/) 5. [Многопоточность POSIX Threads](practice/pthread/) 6. [Мьютексы, условные переменные, атомарные переменные](practice/mutex-condvar-atomic/) 7. [Низкоуровневое сетевое взаимодействие](practice/sockets-udp/) ### Модуль 4 (весна 2022) 1. [Библиотеки функций и их загрузка](practice/function-pointers) 2. [Часть 1: Система сборки CMake](practice/linux_basics/cmake.md). [Часть 2: Протокол HTTP/1.1 и cURL](practice/http-curl/) 3. [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) 4. [Часть 1: Работа с каталогами в POSIX](practice/posix_dirent_time). [Часть 2: Файловые системы FUSE](practice/fuse/) 5. [Python Extending and Embedding](practice/python) ================================================ FILE: 2021-2022-half-year-course.md ================================================ # Операционные системы на ИВТ ### Осень 2020 1. [Введение в Linux и базовые инструменты разработки](practice/linux_basics/) 2. [Командный интерпретатор bash и обработка текстов](practice/bash-grep-sed/) 3. [Системные вызовы](practice/asm/nostdlib_baremetal/) 4. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 5. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 6. [Отображение файлов на память](practice/mmap/) 7. [Запуск и завершение работы процессов](practice/fork/) 8. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) 9. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 10. [Сигналы. Часть 1](practice/signal-1/) [Сигналы. Часть 2](practice/signal-2/) 11. [Сокеты TCP/IP](practice/sockets-tcp/) 12. [Мультиплексирование ввода-вывода](practice/epoll/) 13. [Сокеты UDP и AF_PACKET](practice/sockets-udp/) [Berkley Packet Filter](practice/bpf/) 14. [Часть 1: Работа с каталогами в POSIX](practice/posix_dirent_time). [Часть 2: Файловые системы FUSE](practice/fuse/) 15. [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) ================================================ FILE: 2021-2022.md ================================================ # АКОС - Operating Systems Course ![Лицензия Creative Commons](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) Все материалы доступны по лицензии [Creative Commons «Attribution-ShareAlike» («Атрибуция-СохранениеУсловий») 4.0 Всемирная](http://creativecommons.org/licenses/by-sa/4.0/). ## Материалы курса 2021/2022 учебного года * [Направления ПМФ, ИВТ в бакалавриате ФПМИ, и магистратура Блокчейн на ЛФИ](2021-2022-half-year-course.md) * [Направление ПМИ в бакалавриате ФПМИ](2021-2022-full-year-course.md) ## Конфиги В курсе используется проверка корректности стиля форматирования, и в случае несоответствия, решения задач не принимаются. Предварительно необходимо выполнять форматирование кода с помощью `clang-format`. [Конфиг для clang-format](practice/.clang-format) ## Материалы предыдущих лет * [Архив: 2020-2021 учебный год](2020-2021.md) * [Архив: 2019-2020 учебный год](2019-2020.md) * [Архив: 2018-2019 учебный год](2018-2019.md) ================================================ FILE: 2022-2023-full-year-course.md ================================================ # АКОС на ПМИ ### Модуль 1 (осень 2022) 1. [Введение в Linux и базовые инструменты разработки](practice/linux_basics/) 2. [Командный интерпретатор bash и утилита sed](practice/bash-grep-sed) 3. [Целочисленная и вещественная арифметика](practice/math) 4. [Архитектура AArch64 и язык ассемблера](practice/aarch64) 5. [Архитектура AArch64 - стек и вызов функций](practice/aarch64-functions) 6. [Архитектура x86-64 и язык ассемблера](practice/x86-64) ### Модуль 2 (осень 2022) 1. [Системные вызовы](practice/asm/nostdlib_baremetal/) 2. [Низкоуровневый файловый ввод и вывод](practice/file_io/) 3. [Аттрибуты файлов и файловых дескрипторов](practice/stat_fcntl/) 4. [Отображение файлов на память](practice/mmap/) 5. [Запуск и завершение работы процессов](practice/fork/) 6. [Запуск программ через fork-exec](practice/exec-rlimit-ptrace/) ### Модуль 3 (весна 2023) 1. [Многопоточность POSIX Threads](practice/pthread/) 2. [Мьютексы, условные переменные, атомарные переменные](practice/mutex-condvar-atomic/) 3. [Копии файловых дескрипторов и неименованные каналы](practice/fdup-pipe/) 4. [Сигналы](practice/signals/) 5. [Сокеты TCP/IP](practice/sockets-tcp/) 6. [Низкоуровневое сетевое взаимодействие](practice/sockets-udp/) 7. [Мультиплексирование ввода-вывода](practice/epoll/) ### Модуль 4 (весна 2023) 1. [Библиотеки функций и их загрузка](practice/function-pointers) 2. [Часть 1: Система сборки CMake](practice/linux_basics/cmake.md). [Часть 2: Протокол HTTP/1.1 и cURL](practice/http-curl/) 3. [Шифрование с использованием OpenSSL/LibreSSL](practice/openssl/) 4. [Часть 1: Работа с каталогами в POSIX](practice/posix_dirent_time). [Часть 2: Файловые системы FUSE](practice/fuse/) 5. [Python Extending and Embedding](practice/python) ================================================ FILE: 2022-2023.md ================================================ # АКОС - Operating Systems Course ![Лицензия Creative Commons](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) Все материалы доступны по лицензии [Creative Commons «Attribution-ShareAlike» («Атрибуция-СохранениеУсловий») 4.0 Всемирная](http://creativecommons.org/licenses/by-sa/4.0/). ## Материалы курса 2022/2023 учебного года * [Направления ПМФ, ИВТ в бакалавриате ФПМИ](2021-2022-half-year-course.md) * [Направление ПМИ в бакалавриате ФПМИ](2022-2023-full-year-course.md) ## Конфиги В курсе используется проверка корректности стиля форматирования, и в случае несоответствия, решения задач не принимаются. Предварительно необходимо выполнять форматирование кода с помощью `clang-format`. [Конфиг для clang-format](practice/.clang-format) ## Материалы предыдущих лет * [Архив: 2021-2022 учебный год](2021-2022.md) * [Архив: 2020-2021 учебный год](2020-2021.md) * [Архив: 2019-2020 учебный год](2019-2020.md) * [Архив: 2018-2019 учебный год](2018-2019.md) ================================================ FILE: LICENSE ================================================ Attribution-ShareAlike 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-ShareAlike 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. Additional offer from the Licensor -- Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. c. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: en-mipt/Exam-Fall.md ================================================ 1. What is an Operating System, describe it's purpose. Linux basic concepts: services, users, sessions and processes. 2. Software for Linux. Packages and repositories. Building from the source code. Describe differences between various approaches. 3. Source code compilation. Describe the stages of compilation. Differences between C and C++ languages. The way to use both C and C++ in the same project. 4. Integer numbers representation and bitwise operations. Signed and unsigned integers. Integer overflows and the ways to detect them. 5. Floating point implementation. The real number types. Special IEEE754 values. 6. Assembly languages and processor instruction set architectures. Difference between RISC and CISC. Program layout in memory. 7. Interrupts and interrupt handling. Software interrupts. BIOS and Kernel purpose. System calls and calling conventions for Linux/x86. 8. UNIX Virtual File System. File system organization and file types. Describe the difference between hard links and symbolic links. Physical file systems. 9. File descriptors: purpose and relationship to virtual file system. Low-level file operations provided by The Kernel. 10. Memory layout. Describe static memory, stack and heap. Physical address space and processe's virtual address space. Memory paging on x86. 11. Processes in UNIX systems. Process attributes and lifetime states. Zombie process problem. 12. UNIX signals. Standard signals purpose and default behaviour. Singal handling for System-V-style and BSD-style systems. Describe Async-Safety problem. 13. UNIX signals delivering. Pending signals mask and signal mask for process. POSIX Extended signals ("realtime extension"). 14. Compiled software libraries. Static and dynamic libraries. Position-Independent Code. Libraries linkage and loading. 15. Function replace techniques. Link-stage wrapping and library forced preloading. Describe differences in implementation and appliсation area. ================================================ FILE: en-mipt/README.md ================================================ # Operating Systems Course for Foreigners Program **Important!** The Fall Midterm exam will take place: * Dec, 19 (Thursday) - Early Exam at Timka Building in Moscow * TBA: from Dec, 23 to Dec, 26 - The Main Exam at Dolgoprudny MIPT campus The exam program is [available here](Exam-Fall.md). --- Our primary operating system is the Linux. You can use [this VirtualBox image](https://drive.google.com/file/d/19pvmNOhqSQG_ZGx6kZ2hbhcuVefShDmI/view?usp=sharing). Regular user name for this image is `student`, password `qwerty`. The root user password is the same. The contest for your homeworks is here: [http://ejudge64.atp-fivt.org](http://ejudge64.atp-fivt.org) ## Part I. Fall 2019 1. [Lesson 01. Introduction to UNIX-like systems and Linux](linux-basics/linux-intro.md) 2. [Lesson 02. Administration Basics](admin-basics/) 3. [Lesson 03. Developer Tools for C/C++ Lanuages](dev-tools/dev-tools.md) 4. [Lesson 04. Numbers And Structures Representation](numbers/) 5. [Lesson 05. Assembly Language. Memory Access, Stack and Heap](arm/arm.md) 6. [Lesson 06. System Calls versus Functions](syscalls/) 7. [Lesson 07. File Descriptors and Low-Level File Operations](fds/) 8. [Lesson 08. File Attributes](stat/) 9. [Lesson 09. Posix Time Representation](time/) 10. [Lesson 10. Memory Mapping](mmap/) 11. [Lesson 11. Processes Creation and Lifecycle](processes-1/) 12. [Lesson 12. Process Spawning and Restriction](processes-2/) 13. [Lesson 13. Pointers to Functions. Runtime Libraries Loading](dlopen/) 14. Midterm Exam ================================================ FILE: en-mipt/admin-basics/README.md ================================================ # Linux Administration There is no special topics to cover. Just read [this full reference](https://www.tldp.org/LDP/sag/html/sag.html) when you'll face a problem. ================================================ FILE: en-mipt/arm/arm.md ================================================ # ARM assembler basics ## Writing and compiling programs Assembly language programs for the GNU compiler are saved in a file whose name ends in `.s` or `.S`. In the case of `.S` it is assumed that the text of the program can be processed by the preprocessor. One of the commands is used to compile: `arm-linux-gnueabi-as` or `arm-linux-gnueabi-gcc`. In the first case, the text is only compiled into an object file, in the second – into an executable program, linked with the standard C library, from which you can use I/O functions. ARM processors support two sets of commands: the main 32-bit `arm`, and the compacted 16-bit `thumb`. And the processor is able to switch between them. In this workshop, we will use a 32-bit instruction set, so the texts should be compiled with the `-marm` option. ## General syntax ``` // This is a comment (like in C++) .text // the beginning of the section .text with program code .global f // indicates that the f label // is externally accessible (similar to extern) f: // label (ends with a colon) // series of commands mul r0, r0, r3 mul r0, r0, r3 mul r1, r1, r3 add r0, r0, r1 add r0, r0, r2 mov r1, r0 bx lr ``` ## Registers The processor can only perform operations on *registers* - 32-bit memory cells in the processor core. ARM has 16 registers available programmatically: `r0`, `r1`, ... ,`r15`. Registers `r13`...`r15` has special assignments and extra names: * `r15` = `pc`: Program Counter - pointer to the currently executing instruction * `r14` = `lr`: Link Register - stores the return address from the function * `r13` = `sp`: Stack Pointer - pointer to the top of the stack. ## Flags Commands execution may lead to some additional information that is stored in the *flag register*. Flags refer to the last command executed. The main flags are: * `C`: Carry - an unsigned overflow occurred * `V`: oVerflow - a signed overflow occurred * `N`: Negative - negative result * `Z`: Zero - zeroing the result. ## Commands For a complete list of 32-bit commands, see [this reference](/practice/asm/arm_basics/arm_reference.pdf), starting at page 151. The ARM-32 architecture implies that almost all commands can have *conditional execution*. The condition is encoded with 4 bits in the command itself, and in terms of Assembly syntax, commands can have suffixes. Thus, each command consists of two parts (without spaces): the command itself and its suffix. ## Basic arithmetic operations * `AND regd, rega, argb` // regd ← rega & argb * `EOR regd, rega, argb` // regd ← rega ^ argb * `SUB regd, rega, argb` // regd ← rega − argb * `RSB regd, rega, argb` // regd ← argb - rega * `ADD regd, rega, argb` // regd ← rega + argb * `ADC regd, rega, argb` // regd ← rega + argb + carry * `SBC regd, rega, argb` // regd ← rega − argb − !carry * `RSC regd, rega, argb` // regd ← argb − rega − !carry * `TST rega, argb` // set flags for rega & argb * `TEQ rega, argb` // set flags for rega ^ argb * `CMP rega, argb` // set flags for rega − argb * `CMN rega, argb` // set flags for rega + argb * `ORR regd, rega, argb` // regd ← rega | argb * `MOV regd, arg` // regd ← arg * `BIC regd, rega, argb` // regd ← rega & ~argb * `MVN regd, arg` // regd ← ~argb ## Suffixes-conditions ``` EQ equal (Z) NE not equal (!Z) CS or HS carry set / unsigned higher or same (C) CC or LO carry clear / unsigned lower (!C) MI minus / negative (N) PL plus / positive or zero (!N) VS overflow set (V) VC overflow clear (!V) HI unsigned higher (C && !Z) LS unsigned lower or same (!C || Z) GE signed greater than or equal (N == V) LT signed less than (N != V) GT signed greater than (!Z && (N == V)) LE signed less than or equal (Z || (N != V)) ``` ## Transitions The `pc` counter is automatically incremented by 4 when executed another instruction. Commands are used to branch programs: * `B label` - the transition to the label; is used inside of functions for branches associated with loops or conditions * `BL label` - save current `pc` to `lr` and switch to `label`; usually used to call functions * `BX register` - go to the address specified in the register; usually used to exit functions. ## Memory operation The processor can only perform operations on registers. Special register loading/saving instructions are used to interact with the memory. * `LDR regd, [regaddr]` – loads the machine word from memory from the address stored in regaddr and stores it in the regd register * `STR reds, [regaddr]` – stores the machine word in memory at the address, specified in the regaddr register. # Development for ARM architecture ## Cross-compilation The process of building programs for a different processor architecture or operating system is called cross-compilation. This requires a special version of the `gcc` compiler, designed for a different platform. Many distributions have separate compiler packages for other platforms, including ARM. In addition, you can download an all-in-one delivery for the ARM architecture from the Linaro project: [http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/arm-linux-gnueabi/](http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/arm-linux-gnueabi/). Full `gcc` command names have the *triplet* form: ``` ARCH-OS[-VENDOR]-gcc ARCH-OS[-VENDOR]-g++ ARCH-OS[-VENDOR]-gdb etc. ``` where `ARCH` is the architecture name: `i686`, `x86_64`, `arm`, `ppc`, etc.; `OS` -- the operating system, e.g. `linux`, `win32` or `darwin`; `VENDOR` (optional triplet fragment) -- binary interface agreements (if there are several of them for the platform, for example for ARM this can be a `gnueabi` (standard Linux agreement) or `none-eabi` (no OS, just bare hardware). The name of the architecture for ARM is often distinguished between `arm` (soft float) and `armhf` (hard float). In the first case, the absence of a floating-point block is implied, so all operations are emulated by software, in the second case they are performed by hardware. ## Running programs for non-native architectures Execution of programs designed for other architectures is possible only by interpretation of a foreign set of commands. *Emulators* -- special programs intended for this purpose. ARM architecture, like many other architectures, is supported by the [QEMU emulator](https://www.qemu.org/). You can emulate either a computer system as a whole, similar to VirtualBox, or only a set of processor commands, using the environment of the Linux host system. ### Running ARM binaries in the native environment This emulator is included in all common distributions. QEMU commands are like: ``` qemu-ARCH qemu-system-ARCH ``` where `ARCH` is the name of the architecture to be emulated. Commands, that have `system` in their names, start emulation of a computer system, and you must install an operating system to use them. Commands without`system` in their names require an executable file name as a mandatory argument in Linux, and emulate only a set of processor commands in *user mode*, executing a "foreign" executable file as if it were a normal program. Since most programs compiled for ARM Linux use the standard C library, it is necessary to use the ARM version of glibc. A minimal environment with the necessary libraries can be taken from the Linaro project (see link above), and passed to qemu using the `-L PATH_K_SYSROOT` option. Compile and run example: ``` # assuming the compiler is unpacked in /opt/arm-gcc, # and sysroot -- in /opt/arm-sysroot # Compile > /opt/arm-gcc/bin/arm-linux-gnueabi-gcc -o program hello.c # The output is an executable file that cannot be executed > ./program bash: ./program: cannot execute binary file: Exec format error # But we can run it with qemu-arm > qemu-arm -L /opt/arm-sysroot ./program Hello, World! ``` ### Running ARM programs in Raspberry Pi environment emulation The ideal option for testing and debugging is to use real hardware, such as Raspberry Pi. If you do not have a computer with an ARM-processor, you can perform PC emulation with Raspbian system installed. You can download the image from here: [Google Drive](https://drive.google.com/open?id=11lc_f-_crhP-CJi_FEYb4DE0u9TMViT4) ================================================ FILE: en-mipt/dev-tools/dev-tools.md ================================================ # Developer tools ## Compilers: `gcc` and `clang` The standard delivery of modern UNIX systems includes one of the compilers: either `gcc` or `clang`. By default, `gcc` is used in Linux and `clang` -- in BSD-systems. Working with `gcc` compiler will be described below. But keep in mind that working with `clang` is very similar. Both compilers have a lot in common, including command-line options. In addition, there is the `cc` command, which is a symbolic link to the default C compiler (`gcc` or `clang`), and the `c++` command, which is a symbolic link to the default C++ compiler. Let's consider a simple program in C++: ``` // file hello.cpp #include int main() { std::cout << "Hello, World!" << std::endl; return 0; } ``` You can compile this program by using the command: ``` > c++ -o program.jpg hello.cpp ``` The compiler option `-o FILENAME` specifies the name of the output file to be created. The default name is `a.out`. Pay attention that `program.jpg` is an executable file! ### Stages of compiling a С or C++ program When you run `c++ -o program.jpg hello.cpp` command, a quite complex chain of actions is performed: 1. *Preprocessing* of text file `hello.cpp`. At this stage, the *preprocessor directives* are processed (that begin with the `#` character), and after that the new program text is obtained. If you run the compiler with the `-E` option, only this step will be executed, and the converted text of the program will be output to standard output stream (stdout). 2. *Translating* one or more C/C++ texts into object modules that contain machine code. If you specify the `-c` option, the build of the program will be stopped at this stage and object files with the suffix `o` will be created. Object files contain *binary* executable code that corresponds exactly to some Assembly language text. This text can be obtained using the `-S` option. In this case, text files with the suffix `.s` will be created instead of object files. 3. *Linking* one or more object files into an executable file and linking it to the C/C++ standard library (or other libraries, if required). The compiler calls the third-party program `ld` to perform the linking. ### C programs v.s. C++ programs The `gcc` compiler has the `-x LANGUAGE` option to specify the source language of the program: C (`c`), C++ (`c++`) or Fortran (`fortran`). By default, the source language is defined according to the filename: `.c` is a program in C language, and the file with the name ending in `.cc`, `.cpp` or `.cxx` -- is a C++ text. Therefore, the filename is significant. This applies to the preprocessing and translation stages, but it can cause problems in the build stage, too. For example, when using the `gcc` command instead of `g++` (or `cc` instead of `c++`) you can successfully compile a program's source code in C++, but you will encounter errors during the linking phase because the options passed to the `ld` linker bind only to the C standard library, not to C++ one. Therefore, when building programs in C++, you need to use the command `c++` or `g++`. ### Standard specifying The compiler option `-std=NAME ' allows you to explicitly specify the language standard to be used. It is recommended to specify explicitly the standard to use because the default behavior depends on the Linux distribution you are using. Valid names: * `c89`, `c99`, `c11`, `gnu99`, `gnu11` for C; * `c++03`, `c++11`, `c++14`, `c++17`, `gnu++11`, `gnu++14`, `gnu++17` for C++. A double-digit number in the name of standard indicates its year. If `gnu` is in the standard name, GNU compiler extensions are implied (specific to UNIX-like systems), and the `#define _DEFAULT_SOURCE` macro is also considered to be defined, that in some cases changes the behaviour of individual functions of the standard library. In the future, we will focus on the standard `c11`, and in some tasks, where it will be explicitly stated -- on its extension `gnu11`. ## Object files, libraries and executable files ### Python interpreter module `ctypes` Consider a [program](my-first-program.c) in C: ``` /* my-first-program.c */ #include static void do_something() { printf("Hello, World!\n"); } extern void do_something_else(int value) { printf("Number is %d\n", value); } int main() { do_something(); } ``` Let's compile this program into an object file, and then get from it: (1) an executable program; (2) a shared library. pay attention to the `-fPIC` option for generating position-independent code, which will be discussed in a later seminar. ``` > gcc -c -fPIC my-first-program.c > gcc -o program.jpg my-first-program.o > gcc -shared -o library.so my-first-program.o ``` As a result, we get the program `program.jpg`, which outputs the line `Hello, World!` and a *library* with the name `library.so` that can be both used from C/C++ programs, and dynamic loaded for using by the Python interpreter: ``` > python3 Python 3.6.5 (default, Mar 31 2018, 19:45:04) [GCC] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from ctypes import cdll >>> lib = cdll.LoadLibrary("./library.so") >>> lib.do_something_else(123) >>> retval = lib.do_something_else(123) Number is 123 >>> print(retval) 14 ``` Note that the result of `do_something_else` function is a mysterious number `14` (it may be other when you try to reproduce this experiment), although the function returns `void`. It is so because shared libraries store only the **names** of the functions, not their signatures (parameter and return value types). Attempt to call the `do_something` function will fail: ``` >>> lib.do_something() Traceback (most recent call last): File "", line 1, in File "/usr/lib64/python3.6/ctypes/__init__.py", line 361, in __getattr__ func = self.__getitem__(name) File "/usr/lib64/python3.6/ctypes/__init__.py", line 366, in __getitem__ func = self._FuncPtr((name_or_ordinal, self)) AttributeError: ./library.so: undefined symbol: do_something ``` In this case, the name `do_something` is not found because in the C source text, the `static` modifier before the function name explicitly prohibits the usage of the function anywhere outside the current source text. ### Display the symbol-table To examine object files, including linked ones, you can use the `objdump` utility. The `--syms` or `-t` options display the separate sections of the executable file that are named with *characters*. Some names are marked as '*UND*', which means that the name is used in the object file, but its location is unknown. The task of the linker is just to find the required names in different object files or dynamic libraries, and then - to substitute the correct address. Some characters are marked as global (the `g` character in the second column of the output) and some are marked as local (the `l` character). Non-global characters are considered *non-exportable*, that means (theoretically) they should not be accessible from the outside. ## Debugger If you compile a program with the `-g ` option, the size of the program will increase because it has now additional sections that contain *debugging information*. Debug information contains information about the correspondence of individual fragments of the program to the source code, and includes line numbers, source file names, type names, functions and variables. This information is used only by the debugger, and has almost no effect on the behavior of the program. Thus, debugging information can be combined with optimization, even with the quite aggressive one (compiler option `-O3`). For debugging use the `gdb` command with the name of the executable file or command as an argument. Basic `gdb` commands: * `run` -- run the program, you can pass arguments after `run`; * `break` -- set a break-point, the parameters for this command may be either a function name or a couple of `FILENAME:LINENUMBER`; * `ni`, `si` -- step over the function or step into respectively; * `return` -- step out of the function; * `continue` -- continue to the next breakpoint or exception; * `print` -- prints the value of a given expression for a current context. Interaction with the debugger is performed in the command line mode. Various integrated development environments (CLion, CodeBlocks, QtCreator) are just graphical shells that use this particular debugger and visualise the interaction with it. A more detailed list of commands can be found in [CheatSheet](https://www.cheatography.com/fristle/cheat-sheets/closed-source-debugging-with-gdb/). ================================================ FILE: en-mipt/dev-tools/my-first-program.c ================================================ /* my-first-program.c */ #include static void do_something() { printf("Hello, World!\n"); } extern void do_something_else(int value) { printf("Number is %d\n", value); } int main() { do_something(); } ================================================ FILE: en-mipt/fds/README.md ================================================ # File input-output ## File descriptors File descriptors are integers that uniquely identify open files within a single program. Typically, when a process starts, descriptors `0`, `1`, and `2` are already occupied by the standard input stream (`stdin`), the standard output stream (`stdout`), and the standard error stream (`stderr`). File descriptors can be created using the `create file` or `open file` operation. ## open/close system calls The `open` system call is intended to create a file descriptor from an existing file, and has a following signature: ``` int open(const char *path, int oflag, ... /* mode_t mode */); ``` The first parameter is the file name (full, or relative to the current directory). The second parameter — the options for opening the file. The third (optional) is the access rights to the file when it is created. Main options for opening files: * `O_RDONLY` - read only; * `O_WRONLY` - write only; * `O_RDWR` - read and write; * `O_APPEND` - write to end of file (appending); * `O_TRUNC` - truncating to length 0; * `O_CREAT` - create a file if it does not exist; * `O_EXCL` - create a file only if it does not exist. If successful, a non - negative descriptor number is returned, and if an error occurs, the value `-1` is returned. ## POSIX error handling The error code of the last operation is stored in the global integer "variable" `errno` (in fact, in modern implementations it is a macro). The values of the codes can be found from the `man` pages, or you can print the error text using the `perror` function. ## POSIX file attributes When creating a file, the required parameter is a set of POSIX attributes of file access. Basically, they are encoded in octal calculus as `0ugo`, where `u` (user) - access rights for the owner of the file, `g` (group) - access rights for all users of the file group, `o` (others) - for users who are neither the file's owner nor members of the file's group. In octal notation values from 0 to 7 correspond to a combination of three bits: ``` 00: --- 01: --x 02: -w- 03: -wx 04: r-- 05: r-x 06: rw- 07: rwx ``` ## POSIX read and write Read and write operations are done using system calls: ``` ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); ``` `buf` is a pointer to the data buffer and `count` is the maximum number of bytes to read / write. Typically, `count` specifies the size of the data buffer when reading, or the amount of data when writing. The return type `ssize_t` is an integer defined in the range `[-1...SIZE_MAX]`, where `SSIZE_MAX` is usually the same as `SSIZE_MAX/2`. The value `-1` is used as an error indication; non-negative values are the number of bytes written / read, which may be less than `count`. If the `read` system call returns `0`, the end of the file has been reached or the input channel has been closed. ## File navigation in POSIX You can move the current position in the file for ordinary files. ``` off_t lseek(int fd, off_t offset, int whence); ``` This system call is intended to move the current pointer to a file. The third parameter `whence` is one of the three standard ways to move: * `SEEK_SET` - explicitly specify a position in the file; * `SEEK_CUR` - move the pointer to a certain offset relative to the current position; * `SEEK_END` - move the pointer to a certain offset relative to the end of the file. The `lseek` system call returns the current position in the file, or `-1` if an error occurs. The type `off_t` is signed and is 32-bit, by default . In order to be able to work with files larger than 2 gigabytes, the value of the preprocessor variable **is determined before connecting the header files**: ``` #define _FILE_OFFSET_BITS 64 ``` In this case, the data type `off_t` becomes 64-bit. You can determine the value of preprocessor variables without changing the source code of the program by passing the `-DKEY=VALUE` option to the compiler: ``` # Compile a program with support for large files gcc -D_FILE_OFFSET_BITS=64 legacy_source.c ``` ## Compiling and running Windows programs from Linux For cross-compilation, the GCC compiler is used with the target system `w64-mingw`. Can be installed from the package: * `mingw32-gcc` - for Fedora * `gcc-mingw-w64` - for Ubuntu * `mingw32-cross-gcc` - for openSUSE. You can compile a program for Windows with the command: ``` $ i686-w64-mingw-gcc -m32 program.c # The output is a.exe file, not a.out ``` Note that the Linux system, unlike Windows, is case-sensitive (distinguishes case of letters in the file system), so you need to use the standard WinAPI header files in lowercase: ``` #include // correct #include // compiles in Windows, but not in Linux ``` You can run the resulting file using WINE: ``` $ WINEDEBUG=-all wine a.exe ``` Setting the environment variable `WINEDEBUG` to `- all` causes the console to not display debug information related to the `wine` subsystem, which is mixed with the output of the program itself. ## File descriptors and other data types in WinAPI For file descriptors in Windows `HANDLE` type is used. For single-byte strings `LPCSTR` is used, for multi-byte strings `LPCWSTR` is used. WinAPI functions that have different string support options and work with single-byte functions - end with the letter `A`, and functions that work with multi-byte files - end with the letter `W`. Type `DWORD` is for an unsigned 32-bit number used for flags. For a full list of data types, check [Microsoft documentation](https://docs.microsoft.com/en-us/windows/desktop/winprog/windows-data-types). ## WinAPI functions for working with files The file can be opened using the [CreateFile function](https://docs.microsoft.com/ru-ru/windows/desktop/api/fileapi/nf-fileapi-createfilea). Read and write — using the [ReadFile](https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-readfile) and [WriteFile](https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-writefile). File navigation-using the [SetFilePointerEx function](https://docs.microsoft.com/ru-ru/windows/desktop/api/fileapi/nf-fileapi-setfilepointerex). ================================================ FILE: en-mipt/linux-basics/linux-intro.md ================================================ # Introduction to Linux OS ## It is not Windows. Forget everything you know. ### Terminology * File system is a hierarchy of files and *directories*. Do not call directories – "folders". * Unlike Windows, all files in UNIX are equal, regardless of their name. The concept of "file extension" does not exist, but there are *name suffixes*, separated from the main name by a period, for readability. The filename may have several suffixes, for example `.tar.gz`. * The system runs a huge number of *processes*, not "tasks". Processes can be started either directly by the user or by one of the *daemons* that are launched at system startup. Daemons are processes in themselves. ### Common notations of keyboard shortcuts The following notations of keyboard shortcuts are commonly used for working with UNIX console programs: * `C-Letter` - simultaneous pressing `Ctrl` and the letter key. Attention to MacOS users: `Ctrl` - is exactly the `Ctrl` key, not the `Command`. * `M-Letter` - simultaneous pressing `Alt` and the letter key. "M" stands for "Meta". There was such a key button on old workstations Sun и SGI. * `C-Letter1 Letter2` - simultaneously press `Ctrl` and `Letter1`, then release `Ctrl` и press `Letter2`. The same is for `Alt`. Shortcut `C-Letter1` is called *prefix* of a keyboard shortcut. Usually keyboard shortcuts are grouped under the same prefixes for actions of the same nature. * `C-Letter1 C-Letter2`. Press `Ctrl`, after that press and release `Letter1`, then press and release `Letter2`. After that you can release `Ctrl` key. * Keys `F13`...`F15`. They are missing on the PC-keyboard. Pressing can be done with `Shift` and one of the functional keys with number `F...` 10 or 12 less, depending on the terminal. For example, in many graphical terminals `Shift+F5` is for pressing `F15`. ## Start working Linux, like any other operating system of the UNIX family, is **multiuser** operating system. To get started it is necessary to know your username and password. There are different ways to login to the system depending on the purposes of using the system. ### Local login with GUI (Graphical User Interface) This option is typically used for installing Linux as a Desktop. Usually most Linux distributions provide automatic login if only one user-human was specified during installation (there is also another kind of users –system-users). In case of multiple users, login to the system is pretty similar to the one in Windows or Mac. After login — a graphical shell (GNOME, Unity or KDE) is displayed. The command line, that we will mainly work with, is available in Terminal app. ### Local login without GUI This option is typically used for initial server setup (the graphics stack is a potential "security hole" and usually is not installed) and also for working with embedded systems. After the system is loaded or the terminal is connected, text message will ask for a user name and a password. And after login to system the control is transferred to the **command interpreter**. ### Remote SSH login To connect via SSH, you need to use the following command (for Linux/Mac) ``` ssh USERNAME@HOSTNAME ``` There are special programs for SSH connection in Windows, for example [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html). After the connection you need to enter your password for login. In some cases there is no need to enter a password, for example, if authorization is configured with using SSH keys. After login to system the control is transferred to the **command interpreter**. ## Command line working basics ### File system navigation The command-line prompt usually has an appearance dependent on status: ``` USERNAME@HOSTNAME:WORKING_DIRECTORY> ``` Root directory in file system hierarchy -- is `/`. After system login the working directory is a *home directory* of current user -- this is a directory that is both readable and writable. The home directories of regular users are located: * `/home/` -- for Linux * `/usr/local/home` -- for FreeBSD * `/Users` -- for MacOS Regardless of the operating system, the name `~` ("tilde" symbol) is synonymous with home directory of the *current user*. Tokens `.` and `..` mean respectively the current directory and the directory one level higher in the hierarchy. To navigate through the directories, use the command `cd`. Examples: ``` cd .. # Navigate one level up cd ../.. # Navigate two levels up cd ../src # Navigate one level up, and then to subdirectory src cd / # Navigate to the root directory cd /usr/lib64 # Navigate to directory /usr/lib64 cd ~/projects # Navigate to directory /home/NAME/projects ``` **Note**. When entering file or directory names, press `TAB` for autocomplete function. ### Run executable files Executable file is any file (including a text file) that has special attribute. There are two ways to run an executable file: * Enter the name of this file, if the file is located in one of the directories listed in the *environmental variable* `PATH`. All standard UNIX system programs are run in this way. * Enter the *full name* of the file. The full name can be either absolute (starts with `/`) or relative (starts with `.`). Programs in the home directory are usually run in this way. ### Default file management programs * `cp` -- copy file or directory (with option `-R`) * `mv` -- rename (move) file or directory (with option `-R`) * `rm` -- remove file or directory (with option `-r`) * `ls` -- list working directory contents All these commands are normal programs that are located in the directory `/usr/bin`. **Question**. Why isn't there a program called `cd`? ### Executable file formats * Binary file starts with a byte sequence `0x7F 0x45 0x4C 0x46`. This format is called ELF (Executable and Linkable Format). * An arbitrary file (including a text file) that starts with a string `#!INTERPRETER_NAME\n`. In this case system starts the specified interpreter and passes the executable file to it as an argument. Executable file example: ``` #!/usr/bin/python print("Hello, UNIX!") ``` ### Midnight Commander Using the command line is not always convenient to navigate the file system. **Note**. When using the provided VM image, the file system is also available via FTP: [ftp://student@192.168.56.105/](ftp://student@192.168.56.105/). *Midnight Commander* -- two-panel file manager available for almost every UNIX-like operating system (including MacOS). It is run with the command `mc`. Working with it is similar to working with FAR Manager or Total Commander. Some keyboard shortcuts are an exception. Main operations: * `F3` -- file view * `F4` -- file edit * `Shift+F4` -- create and edit new file * `F5` -- copy * `F6` -- move * `F7` -- create directory * `F8` -- remove * `F10` -- exit Midnight Commander * `C-x c` -- edit file permissions * `C-x o` -- edit file's user * `C-x s` -- create symbolic link for file **Note.** To exit view or edit mode press `Esc` **twice**. It is so because `Esc` key in classic terminals is intended for prefix input of control characters. ### File system hierarchy Unlike Windows, where each physical disk or partition on the disk corresponds to a specific letter, for example, `C:\`, the file system tree on UNIX systems shares the root `/`. Separate disks or partitions are *mounted* into subdirectories of the main file system. The file system of all Linux distributions has the following hierarchy: * `/bin` -- executable programs that provide the essential commands * `/boot` -- files required to boot the operating system * `/dev` -- pseudo-device files * `/etc` -- system configuration text files * `/home` -- user home directories * `/lib` or `/lib64`, or both of them -- the minimum set of shared libraries required for system availability. The `/lib64` directory is present on 64-bit systems and contains variants of libraries for x86_64, while the `/lib` contains their analogues for i386. * `/lost+found` -- files that are out of any directory for some reason (such as an incorrect shutdown of the computer, or disk failure), but their content is available. * `/media` -- directory for mounting replaceable media available to all users * `/mnt` -- directory for mounting network file systems or foreign sections * `/opt` -- directory for installing third-party applications not from the distribution repository, such as Google Chrome * `/proc` -- a virtual file system with information about the processes running in the system is mounted here * `/root` -- the home directory for the `root` user * `/run` -- contains *sockets* and text files with *process IDs* for running daemons * `/sbin` -- executable files to be run by `root` user; other users do not have this directory enabled in the environment variable `PATH`, and to run them, you need to specify the full path * `/srv` -- files with data for services provided by the system * `/sys` -- virtual file system for viewing and modifying kernel settings * `/tmp` -- Directory for temporary files * `/usr` -- contains a hierarchy similar to the root hierarchy; it contains the files of most of the programs that are installed from distribution *repositories* * `/usr/local` -- similar to `/usr`, but is for installing programs yourself from source * `/var` -- contains data from various daemons, such as database. ## Console text editors ### Midnight Commander internal editor Called by pressing `F4` from the file manager or by command `mcedit FILENAME` as an independent program. Main keys: * `F2` -- save file * `Esc Esc` -- exit * `F3` -- start/end of text selection * `F5` -- copy selected text to current position * `F6` -- move selected text to current position * `F8` -- delete selected text; if no text is selected, -- delete the current line ### VI editor Because Midnight Commander, and accordingly, the `mcedit` editor are not always installed by default, sometimes there is a need to use the editor `vi`, taht is included in the basic set of almost all Linux distributions. The editor is started with the command `vi FILENAME` or as a result of some action that requires text editting (for example, `git commit` command -- to edit a commit message). The editor `vi` can be identified by the black screen, and symbol `~` in all empty lines at the end of the text in the left column of the terminal. After launching the editor is in **command mode**. No need to press alphanumeric keys to enter text in this mode. If this happens, press `C-[` to return to command mode. In command mode the text is navigated by the arrow keys, as well as the `h`, `j`, `k` and `l` keys. In addition to the `vi` text editor, many development environments, such as QtCreator и IntelliJ IDEA, as well as Chrome and Firefox browsers, have plugins that allow you to use VIM-style navigation. So it is better to remember these keys. To switch to **insert mode**, similar to GUI-editors, you need to press the `i` key. To exit this mode, use the shortcut `C-[`. To switch to **replacement mode** -- there is the `o` key, output is similar. The basic commands that you need to remember: * `:w` -- save file * `:e FILENAME` -- open or create a file with the specified name * `:q` -- exiting the editor is possible only if there are no unsaved changes * `:q!` -- force exit from editor without saving * `!COMMAND` -- run a UNIX command without exiting the editor You can get a more detailed guide to `vi` by running `vimtutor` ### Nano editor Some distributions, such as Ubuntu, have the `nano` editor installed by default instead of `vi`. It is an easy to use text editor. It can be identified by the text `GNU nano` in the header at the top of the terminal, and hints about keyboard shortcuts like `^G Get Help` in the basement. The `^` symbol stands for `Ctrl` key. ================================================ FILE: en-mipt/numbers/README.md ================================================ # Numbers Representaion ## Integer data types The minimum addressable data size is "typically" one byte (8 bits). "Typically" -- it means that it is not always, and there are different exotic architectures, where "byte" is 9 bits (PDP-10), or specialized signal processors with a minimum addressable data size of 16 bits (TMS32F28xx). The C standard defines the constant `CHAR_BIT` (in the header file ``), for which it is guaranteed that `CHAR_BIT >= 8`. A data type representing one byte is historically called a "character" -- `char`, which contains exactly `CHAR_BITS` bits. The sign of the type `char` is not defined by the standard. For example, it is a signed data type for the x86 architecture, but unsigned -- for ARM. The gcc compiler options `-fsigned-char` and `-funsigned-char` define this behavior. For other integer data types: `short`, `int`, `long`, `long long`, the C language standard defines the minimum bit size: | Data type | Size | | -----------| ----------------------------------| | `short` | at least 16 bits | | `int` | at least 16 bits, usually 32 bits | | `long` | at least 32 bits | | `long long`| at least 64 bits, usually 64 bits | Therefore, you cannot rely on the number of bits in primitive data types, and you should check it with the help of `sizeof` operator, which returns the `number of bytes`, that is, in most cases, how many blocks of size `CHAR_BIT` fit in the data type. The `long` data type should be treated with extreme caution: on a 64-bit Unix system it is 64-bit, and, for example, on 64-bit Windows it is 32-bit. Therefore, to avoid confusion, this type of data is not allowed. ## Signed and unsigned data types Integer data types can be preceded by modifiers `unsigned` or `signed`, which indicate the possibility of negative numbers. For signed types, the high-order bit defines the sign of a number: the value `1` is for negative sign. The method of internal representation of negative numbers is not regulated by the [C standard](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570), but all modern computers use the reverse additional code. Moreover, paragraph 6.3.1.3.2 of the C language standard defines a method for converting types from signed to unsigned in such a way that leads to coding with an additional reverse code. Thus, the value `-1` is represented as an integer, all bits of which are equal to one. From the point of view of low-level programming, and the C language in particular, the sign of data types determines only the way of applying various operations. ## Data types with a fixed number of bits Data types that are guaranteed to have a fixed number of digits: `int8_t`, `int16_t`, `int32_t`, `int64_t` — for signed, and `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` — for unsigned, are defined in header files: `` (for C99+) and `` (for C++11 and later). ## Integer overflow An integer overflow situation occurs when the result data type does not have enough digits to store the final result. For example, if you add the unsigned 8-bit integers: 255 and 1, you get a result that cannot be represented as an 8-bit value. For **unsigned** numbers the overflow situation is normal, and is equivalent to the operation "addition modulo". For **signed** data types -- it leads to a situation of *Undefined behaviour*. Such situations cannot occur in correct programs. Example: ``` int some_func(int x) { return x+1 > x; } ``` It makes sense that such a program should always return a value of `1` (or `true`), since we know that `x+1` is always greater than `x`. The compiler can use this fact to optimize the code, and always return a true value. Thus, the behavior of the program depends on the optimization options that were used. ## Undefined behaviour control The latest versions of the compilers `clang` and `gcc` (since 6th version) are able to control situations of undefined behavior. You can enable the generation of *managed* program code that uses additional run-time checks. Certainly, it comes at the cost of some performance degradation. Such tools are called *sanitizers*, designed for different purposes. The `-fsanitize=undefined` option is used to enable a sanitizer to monitor the undefined behaviour. ## Overflow control, regardless of sign Integer overflow means the shift of high-order bit, and many processors, including the x86 family, can diagnose this. C and C++ standards do not provide this capability, but the gcc compiler (since the 5th version) provides **non-standard** built-in functions for performing operations with overflow control. ``` // Addition operation bool __builtin_sadd_overflow (int a, int b, int *res); bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res); bool __builtin_uadd_overflow (unsigned int a, unsigned int b, unsigned int *res); bool __builtin_uaddl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res); bool __builtin_uaddll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res); // Subtraction operation bool __builtin_ssub_overflow (int a, int b, int *res) bool __builtin_ssubl_overflow (long int a, long int b, long int *res) bool __builtin_ssubll_overflow (long long int a, long long int b, long long int *res) bool __builtin_usub_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_usubl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_usubll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) // Multiplication operation bool __builtin_smul_overflow (int a, int b, int *res) bool __builtin_smull_overflow (long int a, long int b, long int *res) bool __builtin_smulll_overflow (long long int a, long long int b, long long int *res) bool __builtin_umul_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_umull_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_umulll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) ``` ## Real data types There are two ways to represent real numbers: with a fixed number of digits for the fractional part (fixed-point), and with a variable number of digits (floating-point). Fixed-point representation is often used where guaranteed accuracy to a certain digit is required, such as in Finance. Floating-point representation is more common, and all modern processor architectures operate with this format. There are two main types of floating-point objects that are defined by the C standard: `float` (uses 4 bytes for storage) and `double` (uses 8 bytes). The most significant bit (MSB, also called the high-order bit) The high-order bit in the number representation indicates the sign of a number. Next, in order of bits, the value of *biased exponent* is stored (8 bits for `float` or 11 bits for `double`), followed by the *mantissa* value (23 or 52 bits). The biased exponent is necessary in order to be able to store values with a negative exponent in such a representation. The offset for type `float` is `127`, for type `double` -- `1023`. So, the result value can be calculated like ``` Value = (-1)^S * 2^(E-B) * ( 1 + M / (2^M_bits - 1) ) ``` where `S` is the sign bit, `E` is the biased exponent, `B` is the bias offset (127 or 1023), and `M` is the mantissa value, `M_bits` is the number of bits in the exponent. ## How to get the individual bits of a real number Bitwise operations refer to integer arithmetic, and are not provided for types `float` и `double`. Thus, you need to store a real number in memory, and then read it, interpreting it as an integer. In case of C++, the `reinterpret_cast`operator is used for this. For the C language there are two ways: use pointer casting -- the analog of 'reinterpret_cast', or use the type 'union'. ### Pointers casting ``` // We have some real number that is stored in memory double a = 3.14159; // Get a pointer to this number double* a_ptr_as_double = &a; // Lose type information by casting it to void* void* a_ptr_as_void = a_ptr_as_void; // Void* pointer in C can be assigned to any pointer uint64_t* a_ptr_as_uint = a_ptr_as_void; // Well, then just dereferenced pointer uint64_t b = *a_as_uint; ``` ### The use of a type `union` The `union` type is a data type that is syntactically very similar to the `struct` typeю It means that you can list there several named fields, but conceptually they are completely different data types! If a structure or class has a separate storage space in memoty for each field, this does not happen for `union`, and all fields overlap when placed in memory. Typically, the `union` type is used as a variant data type (in C++ since the 17th standard `std::variant` is provided for this), but as a side effect -- it is convenient to use type casts in a manner of `reinterpret_cast` ь, without using pointers. ``` // We have some real number that is stored in memory double a = 3.14159; // Use union type typedef union { double real_value; uint64_t uint_value; } real_or_uint; real_or_uint u; u.real_value = a; uint64_t b = u.uint_value; ``` ## Special values in IEEE 754 format * Infinity: `E=0xFF...FF`, `M=0` * Minus zero (the result of dividing 1 by minus infinity): `S=1`, `E=0`, `M=0` * NaN: `S=[any]`, `E=0xFF...FF`, `M <> 0` Some processors, such as the x86 architecture, support an extension of the standard that allows you to more efficiently represent a set of numbers whose values are close to zero. Such numbers are called *denormalized*. The feature of a denormalized number is the value of the offset exponent `E=0`. In this case, the numerical value is calculated like: ``` Value = (-1)^S * ( M / (2^M_bits - 1) ) ``` ================================================ FILE: en-mipt/syscalls/README.md ================================================ # x86 assembler and System Calls The main reference for the set of commands [(converted to HTML)](https://www.felixcloutier.com/x86/). Reference for the MMX, SSE, and AVX command sets [on the Intel website](https://software.intel.com/sites/landingpage/IntrinsicsGuide/). Good tutorial on x86 Assembly [on WikiBooks](https://en.wikibooks.org/wiki/X86_Assembly) ## 32-bit assembler on 64-bit systems We will use a 32-bit instruction set. On 64-bit architectures, the GCC compiler option `-m32` is used for this. It is also necessary to install the 32-bit library stack. On Ubuntu it is done with only one command: ``` sudo apt-get install gcc-multilib ``` ## Intel and At&T syntax Historically, there are two x86 Assembly language syntaxes: AT&T syntax – used in UNIX systems, and Intel syntax – used in DOS/Windows. The difference is primarily in the order of command arguments. The gcc compiler uses AT&T syntax by default, but can switch to Intel syntax with `-masm=intel` option. You can also specify the syntax to use in the first line in the text of the program itself: ```nasm .intel_syntax noprefix ``` The parameter `noprefix` after `.intel_syntax` indicates here that in addition to the order of the arguments corresponding to the Intel syntax, register names should not begin with the `%` character, and constants should not begin with the `$` character, as it is customary to do in the AT&T syntax. We will use this syntax because it is the syntax that most of the available documentation and examples are written with, including documentation from processor manufacturers. ## General purpose processor registers Historically, the x86 processor family inherited a set of 8-bit General-purpose registers of the 8080/8085 family called `a`, `b`, `c` and `d`. But since the 8086 processor became 16-bit, the registers were named `ax`, `bx`, `cx`and `dx`. In 32-bit processors they are called `eax`, `ebx`, `ecx` and `edx`, in 64-bit `rax`, `rbx`, `rcx` and `rdx`. In addition, x86 has "dual-purpose" registers, which can be used, among other things, as General-purpose registers, if you use a limited subset of processor commands: * `ebp` - the upper boundary of the stack; * `esi` - index of the array element that is a source for copy operation; * `edi` - index of the array element that is a destination for copy operation. The `esp` register contains a pointer to the lower boundary of the stack, so it is not recommended to use it arbitrarily. ### x86-64 registers 64-bit registers for the x86-64 architecture are named starting with the letter `r`. In addition to the registers `rax`...`rsi`,`rdi` general purpose registers`r9`...`r15` can be used. The stack pointer is stored in `rsp`, the upper bound of the stack frame is stored in `rbp`. The lower 32-bit parts of the `rax`...`rsi`,`rdi`,`rsp`,`rbp` registers can be addressed by the names `eax`...`esi`,`edi`,`esp`, `ebp`. When writing values to 32-bit register names, the highest 32 digits are zeroed, which is acceptable for operations on 32-bit unsigned values. To work with signed 32-bit values, such as the `int` type, you must first perform the *sign extension* operations with the `movslq` command. ## Some instructions **For Intel syntax**, the first argument of the command is the one whose value will be modified, and the second – the one which remains unchanged. ```nasm add DST, SRC /* DST += SRC */ sub DST, SRC /* DST -= SRC */ inc DST /* ++DST */ dec DST /* --DST */ neg DST /* DST = -DST */ mov DST, SRC /* DST = SRC */ imul SRC /* (eax,edx) = eax * SRC - signed */ mul SRC /* (eax,edx) = eax * SRC - unsigned */ and DST, SRC /* DST &= SRC */ or DST, SRC /* DST |= SRC */ xor DST, SRC /* DST ^= SRC */ not DST /* DST = ~DST */ cmp DST, SRC /* DST - SRC, the result is not saved, */ test DST, SRC /* DST & SRC, the result is not saved */ adc DST, SRC /* DST += SRC + CF */ sbb DST, SRC /* DST -= SRC - CF */ ``` **For AT&T syntax** the order of arguments is the opposite. That is, the command `add %eax, %ebx` will calculate the sum of `%eax` and `%ebx` , then save the result in register `%ebx`, which is specified as the second argument. ## Processor flags Unlike ARM processors, where the flag register is updated only if there is a special flag in the command, denoted by a suffix `s`, in Intel: processors flags are always updated by most instructions. The `ZF` flag is set if the result of operation is zero. The `SF` flag is set if the result of the operation is a negative number. The `CF` flag is set if the operation results in a transfer from the highest bit of the result. For example, `CF` is set for addition operation if the result of addition of two unsigned numbers cannot be represented by a 32-bit unsigned number. The `OF` flag is set if the operation results in an overflow of the signed result. For example, when adding, `OF` is set if the result of adding two signed numbers cannot be represented by a 32-bit signed number. Note that both addition `add` and subtraction `sub` operations set both the `CF` and the `OF` flags. Addition and subtraction of signed and unsigned numbers are executed exactly in the same way, and so only one instruction is used for both signed and unsigned operations. The `test` and `cmp` instructions do not save the result but only change the flags. ## Control the execution order of the program Unconditional jump is performed using the `jmp` statement ```nasm jmp label ``` Conditional jumps check combinations of arithmetic flags: ```nasm jz label /* jump, if equal to (zero), ZF == 1 */ jnz label /* jump, if not equal (not zero), ZF == 0 */ jc label /* jump, if CF == 1 */ jnc label /* jump, if CF == 0 */ jo label /* jump, if OF == 1 */ jno label /* jump, if OF == 0 */ jg label /* jump, if greater for signed numbers */ jge label /* jump, if >= for signed numbers */ jl label /* jump, if < for signed numbers */ jle label /* jump, if <= for signed numbers */ ja label /* jump, if > for unsigned numbers */ jae label /* jump, if >= (unsigned) */ jb label /* jump, if < (unsigned) */ jbe label /* jump, if <= (unsigned) */ ``` Function call and return are performed by `call` and `ret` commands ```nasm call label /* pushes the return address to the stack, and jumps to label */ ret /* pulls the return address from the stack and navigates to it */ ``` Besides that there is a complex command for loops organization, which implies that the `ecx` register contains a loop counter: ```nasm loop label /* decreases the ecx value by 1; if ecx==0, then jump to the next instruction, otherwise jump to label */ ``` ## Memory addressing Unlike RISC processors, x86 use **one of the command arguments** as an address in memory. **In AT&T syntax** this addressing is written as: `OFFSET(BASE, INDEX, SCALE)`, where `OFFSET` – is a constant, `BASE` and `INDEX` – are registers, and `SCALE` - is one of the values: `1`, `2`, `4` or `8`. The memory address is calculated as `OFFSET+BASE+INDEX*SCALE`. `OFFSET`, `INDEX` and `SCALE` parameters are optional. In their absence it is implied, that `OFFSET=0`, `INDEX=0`, `SCALE` is equal to the size of the machine word. **Intel syntax** uses a more obvious notation: `[BASE + INDEX * SCALE + OFFSET]`. ## Calling conventions for 32-bit architecture The return value of the 32-bit function type is written to the register `eax`. The pair `eax` and `edx` is used to return the 64-bit value. The called function must store the values of the general-purpose registers `ebx`, `ebp`, `esi` and `edi` on the stack. Arguments can be passed to a function in different ways, depending on the conventions of the ABI. ### Cdecl and stdcall conventions Argument passing conventions used on 32-bit x86 systems. All function arguments are stacked right-to-left, then a function is called that addresses the arguments through a `ebp` or `esp` pointer with some positive offset. Example: ```c char * s = "Name"; int value1 = 123; double value2 = 3.14159; printf("Hello, %s! Val1 = %d, val2 = %g\n", s, value1, value2); ``` Here, before calling `printf`, the values of the variables will be stacked before the function is called: ```nasm push value2 push value1 push s push .FormatString call printf ``` If the `stdcall` convention is used, **called** function must remove its arguments from the stack after they were used. If the `cdecl` convention is used, the **calling** function must remove from the stack those variables that were passed to the called function. In C/C++, the conventions that are used can be specified in functions specifiers, for example: ``` void __cdecl regular_function(int arg1, int arg2); #define WINAPI __stdcall void WINAPI winapi_function(int arg1, int arg2); ``` The `stdcall` convention is now used primarily in the Windows operating system to refer to WinAPI functions. In all other cases on 32-bit systems `cdecl` convention is used. ### fastcall convention If you want to pass some integer arguments to a function, you can use registers, as in the ARM architecture. This agreement is called `fastcall`. The `fastcall` convention is used to call kernel functions (system calls) on UNIX-like systems. In particular, Linux uses the `eax` register to pass the system call number, and the `ebx`, `ecx`, and `edx` registers – to pass integer arguments. A similar approach is used in the x86-64 architecture, where there are more registers available than in the 32-bit x86 architecture. ## Calling conventions for 64-bit architecture AMD64 SystemV ABI Integer arguments are passed sequentially in registers: `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`. If more than 6 arguments are passed, the remaining arguments are passed through the stack. Real arguments are passed through registers `xmm0`...`xmm7`. The return value of the integer type must be stored in `rax`, for real type – in `xmm0`. The called function must store the values of general-purpose registers `rbx`, `rby`, and registers `r12`...`r15` on the stack. In addition, when calling a function for a 64-bit architecture, there is an additional requirement – before calling the function, the stack must be aligned to the boundary of 16 bytes, that is, you must reduce the value of `rsp` so it was a multiple of 16. If a stack is used to pass parameters apart from registers, these parameters must be pinned to the bottom aligned edge of the stack. Functions are guaranteed a 128-byte "red zone" in the stack below the `rsp` register - an area that will not be affected by any external event, such as a signal handler. Thus, it is possible to use memory up to `rsp-128` for addressing local variables. ## Interacting with the outside world in Linux via system calls The Linux operating system implements system calls via interrupt with the number `0x80`. The `eax` register stores the system call number, arguments are passed through the `ebx`, `ecx`, `edx`, `esi`, `edi` registers, and the return value is passed through `eax`. System call numbers on x86 are listed in the file `/usr/include/asm/unistd_32.h`. Some useful system calls: * `exit` (`_exit` in C notation) = `1` - program exit; * `read` = `3` - read from file descriptor; * `write` = `4` - write to file descriptor; * `brk` (`sbrk` in C notation) = `45` - moves the boundary of the program data segment. Example (output `Hello` string): ```asm .text ...... mov eax, 4 // 4 - number for write mov ebx, 1 // 1 - stdout file descriptor mov ecx, hello_ptr // pointer to hello mov edx, 5 // number of bytes in output int 0x80 // Linux system call ...... .data hello: .string "Hello" hello_ptr: .long hello ``` ================================================ FILE: harbour/README.md ================================================ # Operating Systems Course for Harbour Space Program Our primary operating system is the Linux. You can use [this VirtualBox image](https://drive.google.com/file/d/19pvmNOhqSQG_ZGx6kZ2hbhcuVefShDmI/view?usp=sharing). Regular user name for this image is `student`, password `qwerty`. The root user password is the same. The contest for your homeworks is here: [http://ejudge64.atp-fivt.org](http://ejudge64.atp-fivt.org) ## Plan is approximate, it may slightly change during the course. 1. [Lesson 01. Introduction to Unix and developer tools]() 2. [Lesson 02. Data representation. Integer arithmetic](ints/) 3. [Lesson 03. Floating point numbers representation](ieee754/) 4. [Lesson 04. ARM tools. ARM assembly. ARM memory.](arm/arm.md) 5. [Lesson 05. Global variables, constants and C libraries](arm/memory_addressing.md) 6. [Lesson 06. Assembly x86](asm-x86/) 7. [Lesson 07. System calls](../en-mipt/syscalls/) 8. [Lesson 08. Low-level input and output.](../en-mipt/fds/) [File attributes]() 9. [Lesson 09. Low-level file operations](files/) 10. [Lesson 10. Posix Time Representation.](time/) [Sanitizers, memory mapping](mmap/) 11. [Lesson 11. Processes Creation and Lifecycle]() [Process Spawning and Restriction]() 12. [Lesson 12. Pipes.](pipes/) [Signals](signals/) 13. [Lesson 13. Sockets TCP/IP and UDP](scokets/) 14. [Lesson 14. Pointers to Functions. Runtime Libraries Loading](libs/) 15. [Lesson 15. Encryption with OpenSSL](openssl/) ================================================ FILE: harbour/arm/arm.md ================================================ # ARM assembler basics ## Writing and compiling programs Assembly language programs for the GNU compiler are saved in a file whose name ends in `.s` or `.S`. In the case of `.S` it is assumed that the text of the program can be processed by the preprocessor. One of the commands is used to compile: `arm-linux-gnueabi-as` or `arm-linux-gnueabi-gcc`. In the first case, the text is only compiled into an object file, in the second – into an executable program, linked with the standard C library, from which you can use I/O functions. ARM processors support two sets of commands: the main 32-bit `arm`, and the compacted 16-bit `thumb`. And the processor is able to switch between them. In this workshop, we will use a 32-bit instruction set, so the texts should be compiled with the `-marm` option. ## General syntax ``` // This is a comment (like in C++) .text // the beginning of the section .text with program code .global f // indicates that the f label // is externally accessible (similar to extern) f: // label (ends with a colon) // series of commands mul r0, r0, r3 mul r0, r0, r3 mul r1, r1, r3 add r0, r0, r1 add r0, r0, r2 mov r1, r0 bx lr ``` ## Registers The processor can only perform operations on *registers* - 32-bit memory cells in the processor core. ARM has 16 registers available programmatically: `r0`, `r1`, ... ,`r15`. Registers `r13`...`r15` has special assignments and extra names: * `r15` = `pc`: Program Counter - pointer to the currently executing instruction * `r14` = `lr`: Link Register - stores the return address from the function * `r13` = `sp`: Stack Pointer - pointer to the top of the stack. ## Flags Commands execution may lead to some additional information that is stored in the *flag register*. Flags refer to the last command executed. The main flags are: * `C`: Carry - an unsigned overflow occurred * `V`: oVerflow - a signed overflow occurred * `N`: Negative - negative result * `Z`: Zero - zeroing the result. ## Commands For a complete list of 32-bit commands, see [this reference](/practice/asm/arm_basics/arm_reference.pdf), starting at page 151. The ARM-32 architecture implies that almost all commands can have *conditional execution*. The condition is encoded with 4 bits in the command itself, and in terms of Assembly syntax, commands can have suffixes. Thus, each command consists of two parts (without spaces): the command itself and its suffix. ## Basic arithmetic operations * `AND regd, rega, argb` // regd ← rega & argb * `EOR regd, rega, argb` // regd ← rega ^ argb * `SUB regd, rega, argb` // regd ← rega − argb * `RSB regd, rega, argb` // regd ← argb - rega * `ADD regd, rega, argb` // regd ← rega + argb * `ADC regd, rega, argb` // regd ← rega + argb + carry * `SBC regd, rega, argb` // regd ← rega − argb − !carry * `RSC regd, rega, argb` // regd ← argb − rega − !carry * `TST rega, argb` // set flags for rega & argb * `TEQ rega, argb` // set flags for rega ^ argb * `CMP rega, argb` // set flags for rega − argb * `CMN rega, argb` // set flags for rega + argb * `ORR regd, rega, argb` // regd ← rega | argb * `MOV regd, arg` // regd ← arg * `BIC regd, rega, argb` // regd ← rega & ~argb * `MVN regd, arg` // regd ← ~argb ## Suffixes-conditions ``` EQ equal (Z) NE not equal (!Z) CS or HS carry set / unsigned higher or same (C) CC or LO carry clear / unsigned lower (!C) MI minus / negative (N) PL plus / positive or zero (!N) VS overflow set (V) VC overflow clear (!V) HI unsigned higher (C && !Z) LS unsigned lower or same (!C || Z) GE signed greater than or equal (N == V) LT signed less than (N != V) GT signed greater than (!Z && (N == V)) LE signed less than or equal (Z || (N != V)) ``` ## Transitions The `pc` counter is automatically incremented by 4 when executed another instruction. Commands are used to branch programs: * `B label` - the transition to the label; is used inside of functions for branches associated with loops or conditions * `BL label` - save current `pc` to `lr` and switch to `label`; usually used to call functions * `BX register` - go to the address specified in the register; usually used to exit functions. ## Memory operation The processor can only perform operations on registers. Special register loading/saving instructions are used to interact with the memory. * `LDR regd, [regaddr]` – loads the machine word from memory from the address stored in regaddr and stores it in the regd register * `STR reds, [regaddr]` – stores the machine word in memory at the address, specified in the regaddr register. # Development for ARM architecture ## Cross-compilation The process of building programs for a different processor architecture or operating system is called cross-compilation. This requires a special version of the `gcc` compiler, designed for a different platform. Many distributions have separate compiler packages for other platforms, including ARM. In addition, you can download an all-in-one delivery for the ARM architecture from the Linaro project: [http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/arm-linux-gnueabi/](http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/arm-linux-gnueabi/). Full `gcc` command names have the *triplet* form: ``` ARCH-OS[-VENDOR]-gcc ARCH-OS[-VENDOR]-g++ ARCH-OS[-VENDOR]-gdb etc. ``` where `ARCH` is the architecture name: `i686`, `x86_64`, `arm`, `ppc`, etc.; `OS` -- the operating system, e.g. `linux`, `win32` or `darwin`; `VENDOR` (optional triplet fragment) -- binary interface agreements (if there are several of them for the platform, for example for ARM this can be a `gnueabi` (standard Linux agreement) or `none-eabi` (no OS, just bare hardware). The name of the architecture for ARM is often distinguished between `arm` (soft float) and `armhf` (hard float). In the first case, the absence of a floating-point block is implied, so all operations are emulated by software, in the second case they are performed by hardware. ## Running programs for non-native architectures Execution of programs designed for other architectures is possible only by interpretation of a foreign set of commands. *Emulators* -- special programs intended for this purpose. ARM architecture, like many other architectures, is supported by the [QEMU emulator](https://www.qemu.org/). You can emulate either a computer system as a whole, similar to VirtualBox, or only a set of processor commands, using the environment of the Linux host system. ### Running ARM binaries in the native environment This emulator is included in all common distributions. QEMU commands are like: ``` qemu-ARCH qemu-system-ARCH ``` where `ARCH` is the name of the architecture to be emulated. Commands, that have `system` in their names, start emulation of a computer system, and you must install an operating system to use them. Commands without`system` in their names require an executable file name as a mandatory argument in Linux, and emulate only a set of processor commands in *user mode*, executing a "foreign" executable file as if it were a normal program. Since most programs compiled for ARM Linux use the standard C library, it is necessary to use the ARM version of glibc. A minimal environment with the necessary libraries can be taken from the Linaro project (see link above), and passed to qemu using the `-L PATH_K_SYSROOT` option. Compile and run example: ``` # assuming the compiler is unpacked in /opt/arm-gcc, # and sysroot -- in /opt/arm-sysroot # Compile > /opt/arm-gcc/bin/arm-linux-gnueabi-gcc -o program hello.c # The output is an executable file that cannot be executed > ./program bash: ./program: cannot execute binary file: Exec format error # But we can run it with qemu-arm > qemu-arm -L /opt/arm-sysroot ./program Hello, World! ``` ### Running ARM programs in Raspberry Pi environment emulation The ideal option for testing and debugging is to use real hardware, such as Raspberry Pi. If you do not have a computer with an ARM-processor, you can perform PC emulation with Raspbian system installed. You can download the image from here: [Google Drive](https://drive.google.com/open?id=11lc_f-_crhP-CJi_FEYb4DE0u9TMViT4) ================================================ FILE: harbour/arm/memory_addressing.md ================================================ # Addressing data in memory and using library functions * [ARM reference](/practice/asm/arm_basics/arm_reference.pdf) ## Basic commands As is typical for classical RISC architecture, the ARM processor can only perform operations on registers. Separate commands *load* (`ldr`) and *save* (`str`) are used to access memory. General form of commands: ``` LDR{condition}{type} Register, Address STR{condition}{type} Register, Address ``` where `{condition}` - this is a condition of command execution, maybe blank (see previous workshop);`{type}` - data type: * `B` - unsigned byte * `SB` - signed byte * `H` - half-word (16 bit) * `SB`- significant half-word * `D` - double word. If type is not specified in the command name, so the common word is implied. Note that to perform data load/save operations that are smaller than the machine word, the signed commands are singled out separately, which make the careful bit extension with zeros, while retaining the senior signed bit. In case of register loading/saving operations of a pair of registers (double word), the register must have an even number. The second machine word is implied to be in the neighbouring register numbered `Rn+1`. ## Addressing The address looks like: `[R_base {, offset}]`, where `R_base` – the name of the register that contains the base address in memory and the optional parameter `offset` – is the offset from the address. The resulting address is defined as `*R_base + offset`. The offset can be either a register name or a numeric constant encoded into a command. Registers are typically used to index array elements, and constants -- to access structure fields or local variables and arguments relative to `[sp]`. ## Addressing fields of C-structures According to the C standard, fields in the memory of structures are placed according to the following rules: * the order of the fields in memory corresponds to the order of the fields in the structure description * the size of the structure must be a multiple of the size of the machine word * data inside machine words are placed in such a way as to be pinned to their boundaries. Thus, the size of the structure does not always match the sum of the sizes of the individual fields. For example: ``` struct A { char f1; // 1 byte int f2; // 4 bytes char f3; // 1 byte }; // 1 + 4 + 1 = 6 bytes // size(struct A) = 12 bytes ``` In this example, the field `f1` uses the part of the machine word, the field `f2` - has a size of 4 bytes, so it takes the next machine word, and for the field `f3` you have to use another one. A simple rearrangement of the fields saves 4 bytes: ``` struct A { char f1; // 1 byte char f3; // 1 byte int f2; // 4 bytes }; // 1 + 1 + 4 = 6 bytes // size(struct A) = 8 bytes ``` In this case, the `f1` and `f3` fields occupy the same machine word. The GCC compiler has a non-standard `packed` attribute that allows you to create "Packed" structures whose size is equal to the sum of the sizes of its individual fields: ``` struct A { char f1; // 1 byte int f2; // 4 bytes char f3; // 1 byte } __attribute__((packed)); // 1 + 4 + 1 = 6 bytes // size(struct A) = 6 bytes ``` ## Standard C library functions Each function that can be used externally has a text label associated with it in the symbol table. After compilation, an entry in the symbol table determines the memory location where the first function statement is placed. Functions implemented in different object modules but compiled into a single executable file are called in the usual way. The way they are called is no different from calling functions from the same object module. When using *libraries*, they are loaded into a separate memory area, and at the build stage, the location address of the libraries is not known. Moreover, the location of the program itself, in general, is also assumed to be unknown. Such functions that are located in dynamically loaded libraries, including the C standard library, appear in the symbol table with a mark `@plt`. Their implementation in Assembly language looks like: ``` function@plt: // Let's load current PC's IP in a temporary register of IP with some offset. // The table of real functions, which is filled at the stage of // loading the program and dynamic libraries, is located by this offset. add ip, pc, #0 add ip, ip, #OFFSET_TO_TABLE_BEGIN // Load the address value from this table. // This leads to the fact that we jump to the implementation of the real function. ldr pc, [ip, #OFFSET_TO_FUNCTION_INDEX] ``` Thus, functions from external libraries are located as if in the program itself, but indeed they represent a "springboard" for performing real functions. ================================================ FILE: harbour/asm-x86/README.md ================================================ # x86 assembler (32-bit, and a few words about 64-bit) The main reference for the set of commands [(converted to HTML)](https://www.felixcloutier.com/x86/). Reference for the MMX, SSE, and AVX command sets [on the Intel website](https://software.intel.com/sites/landingpage/IntrinsicsGuide/). Good tutorial on x86 Assembly [on WikiBooks](https://en.wikibooks.org/wiki/X86_Assembly) ## 32-bit assembler on 64-bit systems We will use a 32-bit instruction set. On 64-bit architectures, the GCC compiler option `-m32` is used for this. It is also necessary to install the 32-bit library stack. On Ubuntu it is done with only one command: ``` sudo apt-get install gcc-multilib ``` ## Intel and At&T syntax Historically, there are two x86 Assembly language syntaxes: AT&T syntax – used in UNIX systems, and Intel syntax – used in DOS/Windows. The difference is primarily in the order of command arguments. The gcc compiler uses AT&T syntax by default, but can switch to Intel syntax with `-masm=intel` option. You can also specify the syntax to use in the first line in the text of the program itself: ```nasm .intel_syntax noprefix ``` The parameter `noprefix` after `.intel_syntax` indicates here that in addition to the order of the arguments corresponding to the Intel syntax, register names should not begin with the `%` character, and constants should not begin with the `$` character, as it is customary to do in the AT&T syntax. We will use this syntax because it is the syntax that most of the available documentation and examples are written with, including documentation from processor manufacturers. ## General purpose processor registers Historically, the x86 processor family inherited a set of 8-bit General-purpose registers of the 8080/8085 family called `a`, `b`, `c` and `d`. But since the 8086 processor became 16-bit, the registers were named `ax`, `bx`, `cx`and `dx`. In 32-bit processors they are called `eax`, `ebx`, `ecx` and `edx`, in 64-bit `rax`, `rbx`, `rcx` and `rdx`. In addition, x86 has "dual-purpose" registers, which can be used, among other things, as General-purpose registers, if you use a limited subset of processor commands: * `ebp` - the upper boundary of the stack; * `esi` - index of the array element that is a source for copy operation; * `edi` - index of the array element that is a destination for copy operation. The `esp` register contains a pointer to the lower boundary of the stack, so it is not recommended to use it arbitrarily. ### x86-64 registers 64-bit registers for the x86-64 architecture are named starting with the letter `r`. In addition to the registers `rax`...`rsi`,`rdi` general purpose registers`r9`...`r15` can be used. The stack pointer is stored in `rsp`, the upper bound of the stack frame is stored in `rbp`. The lower 32-bit parts of the `rax`...`rsi`,`rdi`,`rsp`,`rbp` registers can be addressed by the names `eax`...`esi`,`edi`,`esp`, `ebp`. When writing values to 32-bit register names, the highest 32 digits are zeroed, which is acceptable for operations on 32-bit unsigned values. To work with signed 32-bit values, such as the `int` type, you must first perform the *sign extension* operations with the `movslq` command. ## Some instructions **For Intel syntax**, the first argument of the command is the one whose value will be modified, and the second – the one which remains unchanged. ```nasm add DST, SRC /* DST += SRC */ sub DST, SRC /* DST -= SRC */ inc DST /* ++DST */ dec DST /* --DST */ neg DST /* DST = -DST */ mov DST, SRC /* DST = SRC */ imul SRC /* (eax,edx) = eax * SRC - signed */ mul SRC /* (eax,edx) = eax * SRC - unsigned */ and DST, SRC /* DST &= SRC */ or DST, SRC /* DST |= SRC */ xor DST, SRC /* DST ^= SRC */ not DST /* DST = ~DST */ cmp DST, SRC /* DST - SRC, the result is not saved, */ test DST, SRC /* DST & SRC, the result is not saved */ adc DST, SRC /* DST += SRC + CF */ sbb DST, SRC /* DST -= SRC - CF */ ``` **For AT&T syntax** the order of arguments is the opposite. That is, the command `add %eax, %ebx` will calculate the sum of `%eax` and `%ebx` , then save the result in register `%ebx`, which is specified as the second argument. ## Processor flags Unlike ARM processors, where the flag register is updated only if there is a special flag in the command, denoted by a suffix `s`, in Intel: processors flags are always updated by most instructions. The `ZF` flag is set if the result of operation is zero. The `SF` flag is set if the result of the operation is a negative number. The `CF` flag is set if the operation results in a transfer from the highest bit of the result. For example, `CF` is set for addition operation if the result of addition of two unsigned numbers cannot be represented by a 32-bit unsigned number. The `OF` flag is set if the operation results in an overflow of the signed result. For example, when adding, `OF` is set if the result of adding two signed numbers cannot be represented by a 32-bit signed number. Note that both addition `add` and subtraction `sub` operations set both the `CF` and the `OF` flags. Addition and subtraction of signed and unsigned numbers are executed exactly in the same way, and so only one instruction is used for both signed and unsigned operations. The `test` and `cmp` instructions do not save the result but only change the flags. ## Control the execution order of the program Unconditional jump is performed using the `jmp` statement ```nasm jmp label ``` Conditional jumps check combinations of arithmetic flags: ```nasm jz label /* jump, if equal to (zero), ZF == 1 */ jnz label /* jump, if not equal (not zero), ZF == 0 */ jc label /* jump, if CF == 1 */ jnc label /* jump, if CF == 0 */ jo label /* jump, if OF == 1 */ jno label /* jump, if OF == 0 */ jg label /* jump, if greater for signed numbers */ jge label /* jump, if >= for signed numbers */ jl label /* jump, if < for signed numbers */ jle label /* jump, if <= for signed numbers */ ja label /* jump, if > for unsigned numbers */ jae label /* jump, if >= (unsigned) */ jb label /* jump, if < (unsigned) */ jbe label /* jump, if <= (unsigned) */ ``` Function call and return are performed by `call` and `ret` commands ```nasm call label /* pushes the return address to the stack, and jumps to label */ ret /* pulls the return address from the stack and navigates to it */ ``` Besides that there is a complex command for loops organization, which implies that the `ecx` register contains a loop counter: ```nasm loop label /* decreases the ecx value by 1; if ecx==0, then jump to the next instruction, otherwise jump to label */ ``` ## Memory addressing Unlike RISC processors, x86 use **one of the command arguments** as an address in memory. **In AT&T syntax** this addressing is written as: `OFFSET(BASE, INDEX, SCALE)`, where `OFFSET` – is a constant, `BASE` and `INDEX` – are registers, and `SCALE` - is one of the values: `1`, `2`, `4` or `8`. The memory address is calculated as `OFFSET+BASE+INDEX*SCALE`. `OFFSET`, `INDEX` and `SCALE` parameters are optional. In their absence it is implied, that `OFFSET=0`, `INDEX=0`, `SCALE` is equal to the size of the machine word. **Intel syntax** uses a more obvious notation: `[BASE + INDEX * SCALE + OFFSET]`. ## Calling conventions for 32-bit architecture The return value of the 32-bit function type is written to the register `eax`. The pair `eax` and `edx` is used to return the 64-bit value. The called function must store the values of the general-purpose registers `ebx`, `ebp`, `esi` and `edi` on the stack. Arguments can be passed to a function in different ways, depending on the conventions of the ABI. ### Cdecl and stdcall conventions Argument passing conventions used on 32-bit x86 systems. All function arguments are stacked right-to-left, then a function is called that addresses the arguments through a `ebp` or `esp` pointer with some positive offset. Example: ```c char * s = "Name"; int value1 = 123; double value2 = 3.14159; printf("Hello, %s! Val1 = %d, val2 = %g\n", s, value1, value2); ``` Here, before calling `printf`, the values of the variables will be stacked before the function is called: ```nasm push value2 push value1 push s push .FormatString call printf ``` If the `stdcall` convention is used, **called** function must remove its arguments from the stack after they were used. If the `cdecl` convention is used, the **calling** function must remove from the stack those variables that were passed to the called function. In C/C++, the conventions that are used can be specified in functions specifiers, for example: ``` void __cdecl regular_function(int arg1, int arg2); #define WINAPI __stdcall void WINAPI winapi_function(int arg1, int arg2); ``` The `stdcall` convention is now used primarily in the Windows operating system to refer to WinAPI functions. In all other cases on 32-bit systems `cdecl` convention is used. ### fastcall convention If you want to pass some integer arguments to a function, you can use registers, as in the ARM architecture. This agreement is called `fastcall`. The `fastcall` convention is used to call kernel functions (system calls) on UNIX-like systems. In particular, Linux uses the `eax` register to pass the system call number, and the `ebx`, `ecx`, and `edx` registers – to pass integer arguments. A similar approach is used in the x86-64 architecture, where there are more registers available than in the 32-bit x86 architecture. ## Calling conventions for 64-bit architecture AMD64 SystemV ABI Integer arguments are passed sequentially in registers: `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`. If more than 6 arguments are passed, the remaining arguments are passed through the stack. Real arguments are passed through registers `xmm0`...`xmm7`. The return value of the integer type must be stored in `rax`, for real type – in `xmm0`. The called function must store the values of general-purpose registers `rbx`, `rby`, and registers `r12`...`r15` on the stack. In addition, when calling a function for a 64-bit architecture, there is an additional requirement – before calling the function, the stack must be aligned to the boundary of 16 bytes, that is, you must reduce the value of `rsp` so it was a multiple of 16. If a stack is used to pass parameters apart from registers, these parameters must be pinned to the bottom aligned edge of the stack. Functions are guaranteed a 128-byte "red zone" in the stack below the `rsp` register - an area that will not be affected by any external event, such as a signal handler. Thus, it is possible to use memory up to `rsp-128` for addressing local variables. ================================================ FILE: harbour/files/README.md ================================================ # File properties ## File information ### `stat` structure Each file in the file system is associated with a meta-information (status), which is defined by the `struct stat` structure: ``` struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* Inode number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ /* Backward compatibility */ #define st_atime st_atim.tv_sec #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; ``` You can get meta-information about the file using the command `stat FILENAME` or one of the system calls: * `int stat(const char *file_name, struct stat *stat_buffer)` - getting information about a file by its name; * `int fstat(int fd, struct stat *stat_buffer)` - the same, but for an open file descriptor; * `int lstat(const char *path_name, struct stat *stat_buffer)` - similar to `stat`, but if the file name points to a symbolic link, information about the link itself is returned, not the file it refers to. ### Access modes and file types in POSIX In POSIX there are a few main types of files: * Regular file (`S_IFREG = 0100000`). Takes place on the drive; contains the normal data. * Directory (`S_IFDIR = 0040000`). A special type of file that stores a list of file names. * Symbolic link (`S_IFLNK = 0120000`). A file that references another file (including in a different directory or even on a different file system), and in terms of I/O functions, is no different from the file it references. * Block (`S_IFBLK = 0060000`) и character (`S_IFCHR = 0020000`) devices. Used as a convenient way to interact with the equipment. * Named pipes (`S_IFIFO = 0010000`) and sockets (`S_IFSOCK = 0140000`) for inter-process communication. The file type is encoded in the same structure field with access mode (`rwxrwxrwx`) - integer `.st_mode`. To select individual file types, bitwise operations are performed using one of the macros: `S_ISREG(m)` ` 'S_ISDIR(m)`, `S_ISCHR(m)`, `S_ISBLK(m)`, `S_ISFIFO(m)`, `S_ISLNK(m)' and 'S_ISSOCK(m)`, which return `0` as false and an arbitrary nonzero value as true. To get the access mode, which is encoded in the lower bits`. st_mode`, you can extract them using bitwise operations with the constants `S_IWUSR`, `S_IRGRP`, `S_IXOTH`, etc. A complete list of constants can be found in `man 7 inode`. ### File access Each file, in addition to the access mode (`rwx` for owner, group and others) has two identifiers – positive integers: * `. st_uid` - ID of the file user-owner; * `. st_gid` - ID of the file group-owner. "Owner" permissions are applied when the current user's ID (obtained by `getuid()`) matches the `.st_uid` field. Similarly, for a group – when `getgid()` matches `.st_gid`. Otherwise, the "other" permissions are applied. A convenient way to determine the rights of the current user is to use the system call `access`: ``` int access(const char *path_name, int mode) ``` This system call takes as the `mode` parameter a bitwise combination of the flags `R_OK`, `W_OK`, `X_OK` and `F_OK` — respectively, the ability to read, write, execute a file, and its existence. Returns 0 if the listed attributes are valid for the current user, and -1 otherwise. ## File-creation mask When you create new files using the `open` system call (and all high-level functions that use `open`), you must specify the access mode for the newly created files. In reality, the access mode may differ from the requested one: for a newly created file (or directory), the *file creation mask* is applied using the bitwise "AND-NOT" operation: ``` /* Let umask = 0222 */ open("new_file", O_WRONLY|O_CREAT, 0666); // OK /* Created a file with attributes 0666 & ~0222 = 0444 */ ``` By default, the file creation mask is `0000` — it does not impose any restrictions. The `umask` system call allows you to explicitly set a new mask that can be used to prevent accidental creation of files with too weak access rights. ================================================ FILE: harbour/ieee754/README.md ================================================ # Real numbers representation There are two ways to represent real numbers: with a fixed number of digits for the fractional part (fixed-point), and with a variable number of digits (floating-point). Fixed-point representation is often used where guaranteed accuracy to a certain digit is required, such as in Finance. Floating-point representation is more common, and all modern processor architectures operate with this format. ## Floating point numbers in IEE754 format There are two main types of floating-point objects that are defined by the C standard: `float` (uses 4 bytes for storage) and `double` (uses 8 bytes). The most significant bit (MSB, also called the high-order bit) The high-order bit in the number representation indicates the sign of a number. Next, in order of bits, the value of *biased exponent* is stored (8 bits for `float` or 11 bits for `double`), followed by the *mantissa* value (23 or 52 bits). The biased exponent is necessary in order to be able to store values with a negative exponent in such a representation. The offset for type `float` is `127`, for type `double` -- `1023`. So, the result value can be calculated like ``` Value = (-1)^S * 2^(E-B) * ( 1 + M / (2^M_bits - 1) ) ``` where `S` is the sign bit, `E` is the biased exponent, `B` is the bias offset (127 or 1023), and `M` is the mantissa value, `M_bits` is the number of bits in the exponent. ## How to get the individual bits of a real number Bitwise operations refer to integer arithmetic, and are not provided for types `float` и `double`. Thus, you need to store a real number in memory, and then read it, interpreting it as an integer. In case of C++, the `reinterpret_cast`operator is used for this. For the C language there are two ways: use pointer casting -- the analog of 'reinterpret_cast', or use the type 'union'. ### Pointers casting ``` // We have some real number that is stored in memory double a = 3.14159; // Get a pointer to this number double* a_ptr_as_double = &a; // Lose type information by casting it to void* void* a_ptr_as_void = a_ptr_as_void; // Void* pointer in C can be assigned to any pointer uint64_t* a_ptr_as_uint = a_ptr_as_void; // Well, then just dereferenced pointer uint64_t b = *a_as_uint; ``` ### The use of a type `union` The `union` type is a data type that is syntactically very similar to the `struct` typeю It means that you can list there several named fields, but conceptually they are completely different data types! If a structure or class has a separate storage space in memoty for each field, this does not happen for `union`, and all fields overlap when placed in memory. Typically, the `union` type is used as a variant data type (in C++ since the 17th standard `std::variant` is provided for this), but as a side effect -- it is convenient to use type casts in a manner of `reinterpret_cast` ь, without using pointers. ``` // We have some real number that is stored in memory double a = 3.14159; // Use union type typedef union { double real_value; uint64_t uint_value; } real_or_uint; real_or_uint u; u.real_value = a; uint64_t b = u.uint_value; ``` ## Special values in IEEE 754 format * Infinity: `E=0xFF...FF`, `M=0` * Minus zero (the result of dividing 1 by minus infinity): `S=1`, `E=0`, `M=0` * NaN (signaling): `S=0`, `E=0xFF...FF`, `M <> 0` * NaN (quiet): `S=0`, `E=0xFF...FF`, `M <> 0` Some processors, such as the x86 architecture, support an extension of the standard that allows you to more efficiently represent a set of numbers whose values are close to zero. Such numbers are called *denormalized*. The feature of a denormalized number is the value of the offset exponent `E=0`. In this case, the numerical value is calculated like: ``` Value = (-1)^S * ( M / (2^M_bits - 1) ) ``` ================================================ FILE: harbour/ints/README.md ================================================ # Integer arithmetic ## Integer data types The minimum addressable data size is "typically" one byte (8 bits). "Typically" -- it means that it is not always, and there are different exotic architectures, where "byte" is 9 bits (PDP-10), or specialized signal processors with a minimum addressable data size of 16 bits (TMS32F28xx). The C standard defines the constant `CHAR_BIT` (in the header file ``), for which it is guaranteed that `CHAR_BIT >= 8`. A data type representing one byte is historically called a "character" -- `char`, which contains exactly `CHAR_BITS` bits. The sign of the type `char` is not defined by the standard. For example, it is a signed data type for the x86 architecture, but unsigned -- for ARM. The gcc compiler options `-fsigned-char` and `-funsigned-char` define this behavior. For other integer data types: `short`, `int`, `long`, `long long`, the C language standard defines the minimum bit size: | Data type | Size | | -----------| ----------------------------------| | `short` | at least 16 bits | | `int` | at least 16 bits, usually 32 bits | | `long` | at least 32 bits | | `long long`| at least 64 bits, usually 64 bits | Therefore, you cannot rely on the number of bits in primitive data types, and you should check it with the help of `sizeof` operator, which returns the `number of bytes`, that is, in most cases, how many blocks of size `CHAR_BIT` fit in the data type. The `long` data type should be treated with extreme caution: on a 64-bit Unix system it is 64-bit, and, for example, on 64-bit Windows it is 32-bit. Therefore, to avoid confusion, this type of data is not allowed. ## Signed and unsigned data types Integer data types can be preceded by modifiers `unsigned` or `signed`, which indicate the possibility of negative numbers. For signed types, the high-order bit defines the sign of a number: the value `1` is for negative sign. The method of internal representation of negative numbers is not regulated by the [C standard](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570), but all modern computers use the reverse additional code. Moreover, paragraph 6.3.1.3.2 of the C language standard defines a method for converting types from signed to unsigned in such a way that leads to coding with an additional reverse code. Thus, the value `-1` is represented as an integer, all bits of which are equal to one. From the point of view of low-level programming, and the C language in particular, the sign of data types determines only the way of applying various operations. ## Data types with a fixed number of bits Data types that are guaranteed to have a fixed number of digits: `int8_t`, `int16_t`, `int32_t`, `int64_t` — for signed, and `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` — for unsigned, are defined in header files: `` (for C99+) and `` (for C++11 and later). # Overflow An integer overflow situation occurs when the result data type does not have enough digits to store the final result. For example, if you add the unsigned 8-bit integers: 255 and 1, you get a result that cannot be represented as an 8-bit value. For **unsigned** numbers the overflow situation is normal, and is equivalent to the operation "addition modulo". For **signed** data types -- it leads to a situation of *Undefined behaviour*. Such situations cannot occur in correct programs. Example: ``` int some_func(int x) { return x+1 > x; } ``` It makes sense that such a program should always return a value of `1` (or `true`), since we know that `x+1` is always greater than `x`. The compiler can use this fact to optimize the code, and always return a true value. Thus, the behavior of the program depends on the optimization options that were used. ## Undefined behaviour control The latest versions of the compilers `clang` and `gcc` (since 6th version) are able to control situations of undefined behavior. You can enable the generation of *managed* program code that uses additional run-time checks. Certainly, it comes at the cost of some performance degradation. Such tools are called *sanitizers*, designed for different purposes. The `-fsanitize=undefined` option is used to enable a sanitizer to monitor the undefined behaviour. ## Overflow control, regardless of sign Integer overflow means the shift of high-order bit, and many processors, including the x86 family, can diagnose this. C and C++ standards do not provide this capability, but the gcc compiler (since the 5th version) provides **non-standard** built-in functions for performing operations with overflow control. ``` // Addition operation bool __builtin_sadd_overflow (int a, int b, int *res); bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res); bool __builtin_uadd_overflow (unsigned int a, unsigned int b, unsigned int *res); bool __builtin_uaddl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res); bool __builtin_uaddll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res); // Subtraction operation bool __builtin_ssub_overflow (int a, int b, int *res) bool __builtin_ssubl_overflow (long int a, long int b, long int *res) bool __builtin_ssubll_overflow (long long int a, long long int b, long long int *res) bool __builtin_usub_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_usubl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_usubll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) // Multiplication operation bool __builtin_smul_overflow (int a, int b, int *res) bool __builtin_smull_overflow (long int a, long int b, long int *res) bool __builtin_smulll_overflow (long long int a, long long int b, long long int *res) bool __builtin_umul_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_umull_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_umulll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) ``` ================================================ FILE: harbour/libs/README.md ================================================ # Function libraries and their loading ## Functions and pointers to them Program code in systems with Von Neumann architecture is placed in memory in the same way as regular data. Thus, it can be loaded or generated while the program is running. Some processors allow you to control which areas of memory can be executed and which are not, and in addition, it is controlled by the kernel. Thus, code can only be executed if it is in memory pages that are marked as executable. ## Typification of function pointers A Declaration like ``` int (*p_function)(int a, int b); ``` is interpreted as follows: `p_function` is a pointer to a function that takes two integer arguments, and returns a signed integer. A more general view of a function pointer: ``` typedef ResType (*TypeName)(FuncParameters...); ``` Here `ResType` is the return type of the target function , `TypeName` is the name of type-pointer, `FuncParameters...` - function parameters. The use of the keyword `typedef` is necessary for the C language not to write the entire type every time (similar to `struct`). The declaration of pointers to functions is necessary for compiler to know exactly how to use the address of a function, and make possible for compiler to prepare arguments, and understand where to get the return result of the function. ## Libraries An ELF file can be not only executable file, but also a library that is containing functions. A library differs from an executable in the followinng points: * contains a table of available *symbols* - functions and global variables (you can explicitly specify its creation with the `-E` option); * can be placed arbitrarily, so the program must be compiled into position-independent code with the option `-fPIC` or `-fPIE`; * does not have to have an entry point to the program – functions `_start` and `main`. The library is compiled using the `-shared` option: ``` > gcc -fPIC -shared -o libmy_great_library.so lib.c ``` For Linux and xBSD there is a convention for naming libraries: `libNAME.so`, for Mac - `libNAME.dynlib`, for Windows - `NAME.dll`. Linking a program to a library implies options: * `-lNAME` - specifies the library name without the prefix `lib` and the suffix `.so`; * `-LPATH` - specifies the directory name to search for used libraries. ## Runtime Search Path When an ELF file is loaded, all the necessary libraries (on which it explicitly depends) are loaded. You can view the list of dependencies using the `ldd` command. Libraries are located in one of the default directories: `/lib[64]`, `/usr/lib[64]` or `/usr/local/lib[64]`. Additional directories to search for libraries are defined in the environment variable `LD_LIBRARY_PATH`. It is possible to explicitly define in the ELF file where to look for the necessary libraries. To do this, use the linker option `ld -rpath PATH`. To pass options to `ld`, which is called from `gcc`, you can use the `-Wl, OPTION`. In `rpath` you can specify both absolute paths and the variable `$ORIGIN`, which is equal (when the program is loaded) to the directory containing the program itself. This allows you to create a delivery from program and libraries that are not scattered throughout the file system: ``` > gcc -o program -L. -lmygreat_library program.c \ -Wl,-rpath -Wl,'$ORIGIN/'. ``` This will create an executable file `program` that uses the `libmy_great_library.so`, implying that the library file is in the same directory as the program itself. ## Loading libraries at runtime Libraries do not have to be tied tightly to the program, and can be downloaded as needed. This uses the `dl` feature set, which was included in the 2001 POSIX standard. * `void *dlopen(const char *filename, int flags)` - loads the library file; * `void *dlsym(void *handle, const char *symbol)` - searches the library for the required symbol, and returns its address; * `int dlclose(void *handle)` - closes the library, and unloads it from memory if it is no longer used in the program; * `char *dlerror()` - returns the error text associated with `dl`. If `dlopen` or `dlsym` are unable to open a file or find a character, a null pointer is returned. Example of usage is in files: [lib.c](lib.c) and [dynload.c](dynload.c). ## Position-independent executable file The `-fPIE` option of the compiler indicates that it is necessary to generate position-independent code for `main` and `_start`, and the `-pie` option indicates that you need to specify in the ELF file when linking that it is position-independent. A position-independent executable file in modern systems is placed at a random address. If a position-independent executable also contains a table of exported symbols, it is also a library. If there is no `-shared` option, then the compiler assembles the program by removing the character table from it. The `-Wl,-E` option explicitly saves the character table. Example: ``` # the abc file.c contains int main() { puts("abc"); } > gcc -o program -fPIE -pie -Wl,-E abc.c # the program can be run as a regular program > ./program abc # and can be used as a library > python3 >>> from ctypes import cdll, c_int >>> lib = cdll.LoadLibrary("./program") >>> main = lib["main"] >>> main.restype = c_int >>> ret = main() abc ``` ================================================ FILE: harbour/mmap/README.md ================================================ # Memory pages in virtual address space ## Tools In addition to the `gdb` step-by-step debugger, there are additional tools for detecting problems when working with memory. ### Interpreted execution with `valgrind` The `valgrind` toolset uses controlled execution of program instructions, modifying its code before executing on the physical processor. Main instruments: * `memcheck` - diagnostics of memory problems: incorrect heap pointers, duplicated memory freeing, reading uninitialized data and forgotten memory freeing. * `callgrind` - diagnostics of running program performance. To run a program with valgrind, you should build a program with debug information (compile option `-g`), otherwise the output of valgrind will not be informative. Launching: ``` > valgrind --tool=INSTRUMENT program.jpg ARG1 ARG2 ... ARGn ``` If you use the `callgrind` tool, a `callgrind.out` file is generated after the program is executed. This file has XML format, which can be visualized using KCacheGrind (in KDE for all modern Linux distributions), or its cross-platform equivalent [QCacheGrind](https://sourceforge.net/projects/qcachegrindwin/). ### Runtime error checking with sanitizers It requires new versions of `clang` or `gcc` and allows you to perform instrumental control during program execution much faster than `valgrind`. It is implemented using code generation and some functions replacement, for example replacing `malloc`/`free` with the implementation with additional checks. The main sanitizers: * AddressSanitizer (`-fsanitize=address`) - diagnoses situations of memory leaks, memory double freeing, stack or heap overflow, and stack pointers being used after the function terminates. * MemorySanitizer (`-fsanitize=memory`) - diagnostics of uninitialized data reading situations. Requires the program and all libraries the program is using to be compiled into position-independent code. * Undefined Behavior Sanitizer (`-fsanitize=undefined`) - diagnostics of undefined behavior in integer arithmetic: bit shifts, signed overflow, etc. ## mmap system call ``` #include void *mmap( void *addr, /* recommended mapping address */ size_t length, /* length of the mapping */ int prot, /* desired memory protection flags */ int flags, /* flags for shared mapping */ int fd, /* file descriptor */ off_t offset /* offset relative to the beginning of the file */ ); int munmap(void *addr, size_t length) /* unmap existing mapping */ ``` The `mmap` system call is intended to create an accessible area with a specific address in the virtual address space of a process. This area can be either associated with a specific file (previously opened) or located in RAM. The second case of usage is usually implemented in the `malloc`/`calloc` functions. Memory can only be allocated page by page. For most architectures, the size of a single page is 4Kb, although x86_64 processors support larger pages: 2Mb and 1Gb. In general, you should never rely on a page size of 4096 bytes. It can be found using the `getconf` command or the `sysconf` function: ``` # Bash > getconf PAGE_SIZE 4096 /* C */ #include long page_size = sysconf(_SC_PAGE_SIZE); ``` The `offset` parameter (if a file is used) must be a multiple of the page size; the `length` parameter is not, but the kernel rounds this value to the larger page size. The `addr` parameter (recommended address) can be `NULL`, – in that case the kernel itself assigns an address in the virtual address space. When using file mapping, the `length` parameter is set to the length of the mapped data; if the file size is smaller than the page size, or the last small part of the file is being mapped, the rest of the page is filled with zeros. A memory page can have access attributes: * reading `PROT_READ`; * writing `PROT_WRITE`; * execution `PROT_EXE`; * nothing `PROT_NONE`. In the case of mapping to a file, it must be opened for reading or writing according to the required access attributes. `mmap`flags: * `MAP_FIXED` - requires memory to be allocated to the address specified in the first argument; without this flag, the kernel can select the address closest to the address specified. * `MAP_ANONYMOUS` - to allocate pages in RAM, not to link to file. * `MAP_SHARED` - select pages shared with other processes; in case of file mapping - synchronize changes so that they are available to other processes. * `MAP_PRIVATE` - as opposed to`MAP_SHARED`, do not make mapping available to other processes. In the case of mapping to a file, it is readable, and the changes created by the process are not saved to the file. ================================================ FILE: harbour/openssl/README.md ================================================ # Encryption using OpenSSL / LibreSSL ## Linux Encryption Basics Cryptography in Linux, as in many other UNIX-like systems, is implemented using the `openssl` package or the fork of` libressl` compatible with it. The package provides: * command `openssl` to perform operations on the command line * library `libcrypto` with the implementation of encryption algorithms * library `libssl` with the implementation of interaction via SSL and TLS. ### Calculating Hash Values Commands: * `openssl md5` * `openssl sha256` * `openssl sha512` calculate the hash value for the specified file and output it in a readable form to the standard output stream. The optional option `-binary` indicates the output in binary format. If no filename is specified, a hash value is calculated for the data from the standard input stream. ### Symmetric Encryption Command: ``` openssl enc -CHIPHER -in FILENAME -out EXIT ``` Performs encryption *with a symmetric key*, which means with some “password”, that is the same for both encryption and the reverse decryption operation. A complete list of supported ciphers is displayed with the `openssl enc -ciphers` command. Most commonly used: * `des` is a rather old algorithm using a 56-bit key; * `aes256` or` aes-256-cbc` - more reliable and fast enough; * `base64` - no encryption (key is not required); A convenient way to convert binary files to text representation and vice versa. The `-d` option means inverse conversion, i.e. *decryption*. The `-base64` option implies that the encrypted data is additionally converted to Base64 encoding, for example, to transfer data in the form of text. After running the command, a password and its confirmation will be requested. In the case when you need to automate the launch of the command, the `-pass` option is used, after which it is transmitted how the password is set: * `pass: PASSWORD` - the password is set in plain text as an argument to the command line; terribly unsafe; * `env: VARIABLE` - the password is set by a specific environment variable; a little better, but can be figured out through `/ proc /.../ environ`; * `file: NAME` - the password is taken from the file; * `fd: NUMBER` - the password is taken from the file descriptor with the specified number; used at startup via `fork` +` exec`. Since symmetric encryption algorithms imply the use of a fixed-size key, a text password of arbitrary length is pre-converted using a hash function. By default, SHA-256 is used, but this can be set using the option `-md ALGORITHM`. In addition to the password, the key also includes another component - *salt* of 8 bytes in size, which is stored in the encrypted file itself. This value is randomly generated, but for reproducibility, the result can be explicitly set using the `-S HEX` option, where` HEX` is an eight-byte value in hexadecimal notation. ### Encryption using a key pair The standard algorithm for encryption using a key pair is RSA. Key generation is performed by the command: ``` openssl genrsa -out FILE BIT ``` If the name of the output file is not specified, then the key in text format will be saved to the standard output stream. Typically, RSA keys are stored in files with a suffix of the name `.pem`. Bit depth determines the strength of the key, by default - 2048 bits. Since the private key must be stored somewhere, and in a safe way, it is considered good practice to store it in encrypted form, encryption with a symmetric key is used for this: ``` openssl genrsa -aes256 -passout PASSWORD OPTIONS ``` When using an encrypted private key, it will be necessary to indicate the password specified during its creation each time. The extraction of the public key from the private is carried out by the command: ``` openssl rsa -in PRIVATE_KEY -out PUBLIC_KEY -pubout ``` If encryption was used when creating the key pair, you must enter a password or set it using `-passin`. Public Key Encryption: ``` openssl rsautl -encrypt -pubin -inkey PUBLIC_KEY -in FILE -out EXIT ``` The reverse operation using the private key: ``` openssl rsautl -decrypt -inkey PRIVATE_KEY -in FILE -out EXIT ``` A limitation of the RSA algorithm is that the size of the encrypted data cannot exceed the size of the key. You can deal with this in the following ways: 1. Divide the source data into blocks of 2 or 4 KB in size and encrypt them individually 2. Randomly generate a one-time *session key*, which will be used in conjunction with a symmetric encryption algorithm, but will itself be encrypted using RSA. ``` # Generate a random key 30 bytes long and save # its Base64 textual representation in the $ KEY variable KEY = `openssl rand -base64 30` # We encrypt the symmetric key using the RSA public key echo $ KEY | openssl rsautl -encrypt -pubin \                            -inkey public.pem \                            -out symm_key_encrypted ``` ## Library Here is an example of library usage: ```c #include int main(int argc, char *argv[]) { SHA512_CTX context; SHA512_Init(&context); // hash data SHA512_Final(hash, &context); } ``` ================================================ FILE: harbour/pipes/README.md ================================================ # Duplicating file descriptors. Pipes. ## Duplicating file descriptors The `fcntl` system call allows you to configure various manipulations on open file descriptors. One of the manipulation commands is `F_DUPFD` - creating a *copy* of the descriptor in the current process, but with a different number. A copy implies that two different file descriptors are associated with the same open file in the process, and share the following attributes: * the file object itself; * locks associated with the file; * the current position of the file; * open mode (read /write/add). At the same time, the `CLOEXEC` flag is not saved. I means the automatic closing of the file when the `exec` system call is executed. POSIX system calls `dup` and `dup2` provide simplified semantics for creating a copy of file descriptors: ``` #include /* Returns a copy of the new file descriptor, and, similar to `open`, the numeric value of the new file descriptor is the minimum unoccupied number. */ int dup(int old_fd); /* Creates a copy of a new file descriptor with an explicit new_fd number. If the file descriptor new_fd was previously opened, it closes it.*/ int dup2(int old_fd, int new_fd); ``` ## Unnamed pipes A pipe is a pair of related file descriptors, one of which is read-only and the other is write-only. The channel is created using the `pipe` system call: ``` #include int pipe(int pipefd[2]); ``` As an argument, the system call `pipe` gets a pointer to an array of two integers, where the file descriptor numbers will be written: * `pipefd[0]` - read-only file descriptor; * `pipefd[1]` is a file descriptor intended for writing. ### Writing data to pipe It is done using the system call `write`. It's first argument is `pipefd[1]`. The channel is buffered. In Linux its size is usually 65K. Possible scenarios of writing behavior: * the `write` system call terminates immediately if the data size is smaller than the buffer size and there is enough space in the buffer; * the `write` system call pauses execution until there will be enough space in the buffer (until the previous data will not be read by someone from the channel); * the `write` system call fails with a `Broken pipe` error (delivered via the `SIGPIPE` signal) if the channel on the opposite side has been closed and there is no one to read the data. ### Reading data from a pipe It is done using the system call `read`. It's first argument is `pipefd[0]`. Possible reading behavior scenarios: * if there is data in the pipe's buffer, the system call reads it and exits; * if the buffer is empty and there is **at least one** open file descriptor on the opposite side, the execution of `read` is blocked; * if the buffer is empty and all file descriptors on the opposite side of the pipe are closed system call `read` immediately shuts down, returning `0`. ### dead lock problem When executing `fork`, `dup`, or `dup2` system calls, copies of the file descriptors, associated with the pipe, are created. If you do not close all unnecessary (unused) copies of file descriptors intended for writing, it leads to the fact that when you try to read from the pipe, `read` will be waiting for data instead of shutting down. ``` int fds_pair[2]; pipe(fds_pair); if ( 0!=fork() ) // now we have an implicit copy of the file descriptors { // write some data to the buffer static const char Hello[] = "Hello!"; write(fds_pair[1], Hello, sizeof(Hello)); close(fds_pair[1]); // and now read it back char buffer[1024]; read(fds_pair[0], buffer, sizeof(buffer)); // get deadlock! } else while (1) shched_yield(); ``` To avoid this problem, you should check carefully when copies of file descriptors are created and close them when they are not needed. ================================================ FILE: harbour/signals/README.md ================================================ # Signals. Part 1 ## Introduction A signal is a short message transmission mechanism (signal number), typically interrupting the process to which it was sent. Signals can be sent to the process: * by the kernel, usually in case of a critical execution error; * by other process; * to itself. Signal numbers start with 1. The value 0 has a special purpose (see below about `kill`). Some signal numbers correspond to POSIX-standard names and destinations, which are described in detail by `man 7 signal`. When a signal is received, the process can: 1. Ignore it. It is possible for all signals except `SIGSTOP` and `SIGKILL`. 2. Process with a separate function. Except `SIGSTOP` and `SIGKILL`. 3. Perform the default action specified by the POSIX standard signal assignment. Typically, this is a process shutdown. By default, all signals except `SIGCHILD` (informing about the termination of the child process) and `SIGURG` (informing about the receiving of the TCP segment with priority data), lead to the termination of the process. If a process was terminated with a signal rather than using the `exit` system call, it is considered to have an undefined return code. The parent process can monitor this situation using the `WIFSIGNALED` and `WTERMSIG`macros: ``` pid_t child = ... ... int status; waitpid(child, &status, 0); if (WIFEXITED(status)) { // the child process was terminated via `exit` int code = WEXITSTATUS(status); // return code } if (WIFSIGNALED(status)) { // the child process was terminated by a signal int signum = WTERMSIG(status); // signal number } ``` You can send a signal to any process using the `kill` command. By default, the `SIGTERM` signal is sent, but you can specify which signal to send as an option. In addition, some signals are sent by the terminal, for example Ctrl+C sends a `SIGINT` signal and Ctrl+\ sends a `SIGQUIT ` signal. ## User-defined signals Initially, POSIX reserved two signal numbers that could be used at the discretion of the user: `SIGUSR1` and `SIGUSR2`. In addition, Linux provides a range of signals with numbers from `SIGRTMIN` to `SIGRTMAX`, which can be used at the discretion of the user. The default action for all "user-defined signals" signals is to shut down the process. ## Sending signals programmatically ### System call `kill` Similar to the command of the same name, `kill` is intended to send a signal to any process. ``` int kill(pid_t pid, int signum); // returns 0 or -1 if error ``` You can only send signals to processes that belong to the same user as the user on which the 'kill' system call is executed. The exception is the `root` user, who can do everything. If you try to send a signal to another user's process `kill` will return `-1`. The process number may be less than `1` in cases: * `0` - send a signal to all processes of the current process group; * `-1` - send a signal to all user processes (use with caution!); * negative value `-PID` - send signal to all processes of `PID ' group. The signal number can be set to `0`, in which case no signal will be sent, and `kill` will return `0` if the process (group) with the specified `pid` exists and there are rights to send signals. ### `Raise` and `abort` functions The `raise` function is designed to send a process signal to itself. The standard library function `abort` sends itself a `SIGABRT` signal, and is often used to generate exceptions that can be diagnosed at runtime, such as by the `assert` function. ### System call `alarm` The system call `alarm` starts a timer, after which the process will send itself a signal `SIGALRM`. ``` unsigned int alarm(unsigned int seconds); ``` You can cancel the previously set timer by calling `alarm` with the parameter `0`. The return value is the number of seconds of the previous timer set. ## Обработка сигналов Сигналы, которые можно перехватить, то есть все, кроме `SIGSTOP` и `SIGKILL`, можно обработать программным способом. Для этого необходимо зарегистрировать функцию-обработчик сигнала. ### Системный вызов `signal` ``` #include // Этот тип определен только в Linux! typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); // для Linux void (*signal(int signum, void (*func)(int))) (int); // по стандарту POSIX ``` Системный вызов `signal` предназначен для того, чтобы зарегистрировать функцию в качестве обработчика определенного сигнала. Первым аргументом является номер сигнала, вторым - указатель на функцию, которая принимает единственный аргумент - номер пришедшего сигнала (т.е. одну функцию можно использовать сразу для нескольких сигналов), и ничего не возвращает. Два специальных значения функции-обработчика `SIG_DFL` и `SIG_IGN` предназанчены для указания обработчика по умолчанию (т.е. отмены ранее зарегистрированного обработчика) и установки игнорирования сигнала. Системный вызов `signal` возвращает указатель на ранее установленный обработчик. ### System-V v.s. BSD В стандартах, родоначальниками которых были UNIX System-V и BSD UNIX, используется различное поведение обработчика сигнала, зарегистрированного с помощью `signal`. При определении одного из макросов препроцессора: `_BSD_SOURCE`, `_GNU_SOURCE` или `_DEFAULT_SOURCE` (что подразумевается опцией компиляции `-std=gnu99` или `-std=gnu11`), используется семантика BSD; в противном случае (`-std=c99` или `-std=c11`) - семантика System-V. Отличия BSD от System-V: * В System-V обработчик сигнала выполяется один раз, после чего сбрасывается на обработчик по умолчанию, а в BSD - остается неизменным. * В BSD обработчик сигнала не будет вызван, если в это время уже выполняется обработчик того же самого сигнала, а в System-V это возможно. * В System-V блокирующие системные вызовы (например, `read`) завершают свою работу при поступлении сигнала, а в BSD большинство блокирующих системных вызовов возобновляют свою работу после того, как обработчик сигнала заверщает свою работу. По этой причине, системный вызов `signal` считается устаревшим, и в новом коде использовать его запрещено, за исключением двух ситуаций: ``` signal(signum, SIG_DFL); // сброс на обработчик по умолчанию signal(signum, SIG_IGN); // игнорирование сигнала ``` ### Системный вызов `sigaction` Системный вызов `sigaction`, в отличии от `signal`, в качестве второго аргумента принимает не указатель на функцию, а указатель на структуру `struct sigaction`, с которой, помимо указателя на функцию, хранится дополнительная информация, описывающая семантику обработки сигнала. Поведение обработчиков, зарегистрированных с помощью `sigaction`, не зависит от операционной системы. ``` int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *oldact); ``` Третьим аргументов является указатель на структуру, описывающую обработчик, который был зарегистрирован для этого. Если эта информация не нужна, то можно передать значение `NULL`. Основные поля структуры `struct sigaction`: * `sa_handler` - указатель на функцию-обработчик с одним аргументом типа `int`, могут быть использованы значения `SIG_DFL` и `SIG_IGN`; * `sa_flags` - набор флагов, опиывающих поведение обработчика; * `sa_sigaction` - указатель на функцию-обработчик с тремя параметрами, а не одним (используется, если в флагах присутствует `SA_SIGINFO`). Некоторые флаги, которые можно передавать в `sa_flags`: * `SA_RESTART` - продолжать выполнение прерванных системных вызовов (семантика BSD) после завершения обработки сигнала. По умолчанию (если флаг отсутствует) используется семантика System-V. * `SA_SIGINFO` - вместо функции из `sa_handler` нужно использовать функцию с тремя параметрами `int signum, siginfo_t *info, void *context`, которой помимо номера сигнала, передается дополнительная информация (например PID отправителя) и пользовательский контекст. * `SA_RESETHAND` - после выполнения обработчика сбросить на обработчик по умолчанию (семантика System-V). По умолчанию (если флаг отсутствует) используется семантика BSD. * `SA_NODEFER` - при повторном приходе сигнала во время выполени обработчика он будет обработан немедленно (семантика System-V). По умолчанию (если флаг отсутствует) используется семантика BSD. ## Асинхронность обработки сигналов Сигнал может прийти процессу в любой момент времени. При этом, выполнение текущего кода будет прервано, и будет запущен обработчик сигнала. Таким образом, возникает проблема "гонки данных", которая часто встречается в многопоточном программировании. Существует безопасный целочисленный (32-разрядный) тип данных, для которого гарантируется атомарность чтения/записи при переключении между выполнением основной программы и выполнением обработчика сигнала: `sig_atomic_t`, объявленный в ``. Кроме того, во время выполнения обработчика сигналов запрещено использовать не потоко-безопасные функции (большинство функций стандартной библиотеки). В то же время, использование системных вызовов - безопасно. ================================================ FILE: harbour/sockets/README.md ================================================ # Sockets with connection setup ## Socket A socket is a file descriptor that is open for both reading and writing. It is used for interaction between: * different processes running on the same computer (*host*); * different processes running on different *hosts*. To create a socket use the `socket` system call: ``` #include #include int socket( int domain, // domain type int type, // the type of interaction via the socket int protocol // protocol number or 0 for auto-selection ) ``` The socket mechanism appeared in the 80's of the XX century, when there was no single standard for network communication, and sockets were an abstraction over any network communication mechanism, supporting a huge number of different protocols. In modern systems there are only a few mechanisms defining the socket namespace, that can be considered to be used; others are legacy, that we will not discuss further. * `AF_UNIX` (`man 7 unix`) - a namespace of local UNIX sockets that allow different processes to communicate within the same computer, using as the address a unique name (no longer than 107 bytes) of a special file. * `AF_INET` (`man 7 ip`) - tuple space consisting of 32-bit IPv4 addresses and 16-bit port numbers. The IP address identifies the host for communication on which the process is running. * `AF_INET6` (`man 7 ipv6`) - similar to `AF_INET`, but uses 128-bit IPv6 host addressing; this standard is not yet supported by all hosts and Internet service providers. * `AF_PACKET` (`man 7 packet`) - low level interaction. Sockets usually communicate in one of two ways (specified as the second parameter `type`): * `SOCK_STREAM` - interacts with system calls `read` and `write` as with a regular file descriptor. In the case of network communication, this implies the use of the `TCP` protocol. * `SOCK_DGRAM` - communication without pre-setting interaction to send short messages. In case of communication over the network, this implies the usage of `UDP` protocol. ## Socket pair Sometimes sockets are convenient to use as a mechanism of communication between different threads or related processes: unlike pipes, they are two-way, and in addition, support the processing of the event "close connection". A socket pair is created using the `socketpair` system call: ``` int socketpair( int domain, // In Linux: AF_UNIX only is supported int type, // SOCK_STREAM or SOCK_DGRAM int protocol, // Only value 0 in Linux int sv[2] // An array of two int's (similar to pipe) ) ``` Unlike unnamed pipes, which are created by the `pipe` system call, it does not matter for a pair of sockets which element of the `sv` array is used for reading and which element is used for writing - they are equal. ## Using sockets as a client Sockets can participate in the communication in one of two roles. A process can be a *server*, that means it declares some address (file name, or tuple of IP address and port number) to receive incoming connections, or it can act as a *client*, that means it connects to some server. Once the socket is created, it is not ready yet to interact with the `read` and `write` system calls. The interaction with the server is established using the `connect` system call. After successful execution of this system call, interaction becomes possible before the system call `shutdown` won't be executed. ``` int connect( int sockfd, // the socket file descriptor const struct sockaddr *addr, // a pointer to an *abstract* structure // that describes // the connection address socklen_t addrlen // the size of the real structure // that is passed as // the second parameter ) ``` Since the C language is not object-oriented, it is necessary to pass as an address: 1. A structure whose first field contains an integer with a value that matches the `domain` of the corresponding socket 2. The size of this structure. Particular structures that are "inherited" from the abstract `sockaddr` structure can be: 1. For UNIX address space - structure `sockaddr_un` ``` #include #include struct sockaddr_un { sa_family_t sun_family; // you need to write AF_UNIX char sun_path[108]; // the path to the socket file }; ``` 2. For addressing in IPv4 - structure `sockaddr_in`: ``` #include #include struct sockaddr_in { sa_family_t sin_family; // you need to write AF_INET in_port_t sin_port; // uint16_t port number struct in_addr sin_addr; // a structure of a single field: // - in_addr_t s_addr; // where in_addr_t - is uint32_t }; ``` 3. For addressing in IPv6 - structure `sockaddr_in6`: ``` #include #include struct sockaddr_in6 { sa_family_t sin6_family; // you need to write AF_INET6 in_port_t sin6_port; // uint16_t port number uint32_t sin6_flowinfo; // additional IPv6 field struct in6_addr sin6_addr; // a structure of a single field // declared as union { // uint8_t [16]; // uint16_t [8]; // uint32_t [4]; // }; // i.e. the size of in6_addr is 128 bits uint32_t sin6_scope_id; // additional IPv6 field }; ``` ## IPv4 addresses The host address in IPv4 network is a 32-bit unsigned integer in *network byte order*, that is, Big-Endian. The same is for the port numbers. The byte order conversion from network to system and vice versa is performed using one of the functions declared in ``: * `uint32_t htonl(uint32_t hostlong)` - 32-bit from system to network byte order; * `uint32_t ntohl(uint32_t netlong)` - 32-bit from network to system byte order; * `uint16_t htons(uint16_t hostshort)` - 16-bit from system to network byte order; * `uint16_t ntohs(uint16_t netshort)` - 16-bit from network to system byte order. IPv4 addresses are usually written in decimal notation, separating each byte with a dot, for example: `192.168.1.1`. Such kind of notation can be converted from text to a 32-bit address using the `inet_aton` or `inet_addr` function. ## Closing the network connection The `close` system call is intended to close the *file descriptor*, and it must be called to release an entry in the file descriptor table. This is a necessary but not a sufficient requirement when working with TCP sockets. Besides closing the file descriptor, it is good to notify the opposite sides that the network connection is closing. This notification is done via the `shutdown` system call. ## Using sockets as a server To use a socket as a server, you should do the following: 1. Associate a socket with some address. To do this, use the `bind` system call, whose parameters are exactly the same as for the `connect` system call. If the computer has more than one IP address, the address `0.0.0.0` means `all addresses`. Often when debugging and there is such a problem that the port with a certain number was already busy on the previous run of the program (and, for example, was not correctly closed). It is solved by force reuse of the address: ``` // In the release build you shouldn't do like this! #ifdef DEBUG int val = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val)); #endif ``` 2. Create a queue that will contain incoming but not yet received connections. This is done using the `listen` system call, which takes as a parameter the maximum number of pending connections. For Linux, this value is 128, defined in the constant `SOMAXCONN`. 3. Accept one connection at a time using the `accept` system call. The second and third parameters of this system call can be `NULL` if we are not interested in the address of the one who connected to us. The `accept` system call blocks execution until an incoming connection appears. After that it returns the file descriptor of the new socket, which is associated with a specific client that is connected to us. ## Linux tools for debugging networking ### Network I/O The `nc` command (short for` netcat`) works similarly to the `cat` command, but in the argument is not a file name for outputting a data stream, but a pair `host port`. The `-u` option means sending a UDP packet. If you intend to use only IPv4 addressing, and not IPv6, then option `-4` is used. ``` # Example: transfer data from in.dat to UDP socket on localhost # port 3000 and write the output to the out.dat file > cat in.dat | nc -4 -u localhost 3000> out.dat ``` ### God mode The `wireshark` utility allows you to view absolutely all packets from the` Ethernet` level that pass through the system. This requires `root` privileges, or the` Linux Capabilities` setting for the `/ usr / bin / dumpcap` command, which is part of` wireshark`: ``` sudo setcap cap_net_raw,cap_net_admin+eip /usr/bin/dumpcap ``` Since many network packets pass through the system, you must configure a filter to search only packets of interest. ### Python The Python standard library contains socket tools that exactly match their POSIX counterparts. An example of sending a UDP message: ``` from socket import socket, AF_INET, SOCK_DGRAM IP = "127.0.0.1" PORT = 3000 sock = socket(AF_INET, SOCK_DGRAM) # creating UDP-socket # Connection is not required sock.sendto("Hello!\n", (IP, PORT)) # sending message ``` Recieving UDP-messages: ``` from socket import socket, AF_INET, SOCK_DGRAM IP = "127.0.0.1" PORT = 3000 MAX_SIZE = 1024 sock = socket(AF_INET, SOCK_DGRAM) # creating UDP-socket sock.bind((IP, PORT)) # declaring port while True: data, addr = sock.recvfrom(MAX_SIZE) # recieving message print("Got {} from {}", data, addr) ``` ================================================ FILE: harbour/time/README.md ================================================ ## Working with time ### Current time Time in UNIX systems is defined as the number of seconds that have elapsed since January 1, 1970, and the time is considered as Greenwich mean time (GMT) without daylight saving time (DST). 32-bit systems should cease to exist normally on January 19, 2038, as there will be an overflow of the signed integer type to store the number of seconds. The `time` function returns the number of seconds since the beginning of the epoch. The argument of the function (which can be passed `NULL`) is a pointer to the variable where you want to write the result. In case when it is required an accuracy higher than 1 second, you can use the `gettimeofday` system call, which allows you to get the current time as a structure: ``` struct timeval { time_t tv_sec; // seconds suseconds_t tv_usec; // microseconds }; ``` In this case, despite the fact that the structure defines a field for microseconds, the real accuracy will be about 10-20 milliseconds for Linux. Higher accuracy can be reached with the `clock_gettime` system call. ================================================ FILE: lectures/fall-2018/Lection07-supplementary-01.c ================================================ #include #include #include #include #include int main() { close(2); close(1); /* 1 = */ open("out.txt", O_WRONLY | O_CREAT, 0640); /* 2 = */ open("err.txt", O_WRONLY | O_CREAT, 0640); printf("Hello!"); fprintf(stderr, "World"); static const char buffer3[] = "Number three"; static const char buffer4[] = "Number four"; write(3, buffer3, sizeof(buffer3)-1); lseek(4, 100, SEEK_END); write(4, buffer4, sizeof(buffer4)-1); } ================================================ FILE: lectures/fall-2019/Supplementary-06/lib_and_exec_demo/Makefile ================================================ first: echo "Pass: library, program or lib-and-program" lib-and-program: file.c gcc -o lib-and-program \ -fPIC \ -pie \ -Wl,-E \ file.c program: file.c gcc -o program file.c library: file.c gcc -shared -fPIC -o library.so file.c clean: rm -f library.so rm -f program rm -f lib-and-program ================================================ FILE: lectures/fall-2019/Supplementary-06/lib_and_exec_demo/file.c ================================================ #include void callable_function() { puts("The function that might be called"); } int main(int argc, char *argv[]) { printf("Argc = %d\n", argc); return 100; } ================================================ FILE: lectures/fall-2019/Supplementary-06/lib_and_exec_demo/test.py ================================================ #!/usr/bin/python3 import sys from ctypes import cdll lib = cdll.LoadLibrary("./"+sys.argv[1]) func = lib["callable_function"] main = lib["main"] print("Calling function from lib...") func(); print("Calling main(10, NULL)...") ret = main(10, 0) print("Return value is {}".format(ret)) ================================================ FILE: lectures/fall-2019/Supplementary-06/rpath_demo/Makefile ================================================ first: echo "Pass: library, program-no-rpath or program-with-rpath" program-no-rpath: library ./src/program.c mkdir bin || true gcc -o ./bin/program ./src/program.c \ -L./lib -lmygreatlib program-with-rpath: library ./src/program.c mkdir bin || true gcc -o ./bin/program ./src/program.c \ -L./lib -lmygreatlib \ -Wl,-rpath,'$$ORIGIN/../lib/' library: ./src/mygreatlib.c ./src/mygreatlib.h mkdir lib || true gcc -shared -fPIC -o ./lib/libmygreatlib.so ./src/mygreatlib.c clean: rm -f ./bin/program rm -f ./lib/libmygreatlib.so ================================================ FILE: lectures/fall-2019/Supplementary-06/rpath_demo/src/mygreatlib.c ================================================ #include #include "mygreatlib.h" void say_hello(const char * to_whom) { printf("Hello, %s\n", to_whom); } ================================================ FILE: lectures/fall-2019/Supplementary-06/rpath_demo/src/mygreatlib.h ================================================ #pragma once #ifndef MYGREATLIB_H #define MYGREATLIB_H void say_hello(const char * to_whom); #endif /* MYGREATLIB_H */ ================================================ FILE: lectures/fall-2019/Supplementary-06/rpath_demo/src/program.c ================================================ #include "mygreatlib.h" int main(int argc, char *argv[]) { say_hello(argv[1]); } ================================================ FILE: lectures/fall-2019/Supplementary-06/toyos/Makefile ================================================ AS:=as --32 CC:=gcc -m32 CFLAGS:=-ffreestanding -O2 -Wall -Wextra -nostdlib CPPFLAGS:= LIBS:=-lgcc OBJS:=\ boot.o \ kernel.o \ all: myos.bin .PHONEY: all clean iso run-qemu myos.bin: $(OBJS) linker.ld $(CC) -T linker.ld -Wl,--build-id=none -o $@ $(CFLAGS) $(OBJS) $(LIBS) %.o: %.c $(CC) -c $< -o $@ -std=gnu99 $(CFLAGS) $(CPPFLAGS) %.o: %.s $(AS) $< -o $@ clean: rm -rf isodir rm -f myos.bin myos.iso $(OBJS) iso: myos.iso isodir isodir/boot isodir/boot/grub: mkdir -p $@ isodir/boot/myos.bin: myos.bin isodir/boot cp $< $@ isodir/boot/grub/grub.cfg: grub.cfg isodir/boot/grub cp $< $@ myos.iso: isodir/boot/myos.bin isodir/boot/grub/grub.cfg grub2-mkrescue -o $@ isodir run-qemu: myos.iso qemu-system-i386 -cdrom myos.iso ================================================ FILE: lectures/fall-2019/Supplementary-06/toyos/README.md ================================================ Example from [https://wiki.osdev.org/Bare_Bones](https://wiki.osdev.org/Bare_Bones) ================================================ FILE: lectures/fall-2019/Supplementary-06/toyos/boot.s ================================================ /* Declare constants for the multiboot header. */ .set ALIGN, 1<<0 /* align loaded modules on page boundaries */ .set MEMINFO, 1<<1 /* provide memory map */ .set FLAGS, ALIGN | MEMINFO /* this is the Multiboot 'flag' field */ .set MAGIC, 0x1BADB002 /* 'magic number' lets bootloader find the header */ .set CHECKSUM, -(MAGIC + FLAGS) /* checksum of above, to prove we are multiboot */ /* Declare a multiboot header that marks the program as a kernel. These are magic values that are documented in the multiboot standard. The bootloader will search for this signature in the first 8 KiB of the kernel file, aligned at a 32-bit boundary. The signature is in its own section so the header can be forced to be within the first 8 KiB of the kernel file. */ .section .multiboot .align 4 .long MAGIC .long FLAGS .long CHECKSUM /* The multiboot standard does not define the value of the stack pointer register (esp) and it is up to the kernel to provide a stack. This allocates room for a small stack by creating a symbol at the bottom of it, then allocating 16384 bytes for it, and finally creating a symbol at the top. The stack grows downwards on x86. The stack is in its own section so it can be marked nobits, which means the kernel file is smaller because it does not contain an uninitialized stack. The stack on x86 must be 16-byte aligned according to the System V ABI standard and de-facto extensions. The compiler will assume the stack is properly aligned and failure to align the stack will result in undefined behavior. */ .section .bss .align 16 stack_bottom: .skip 16384 # 16 KiB stack_top: /* The linker script specifies _start as the entry point to the kernel and the bootloader will jump to this position once the kernel has been loaded. It doesn't make sense to return from this function as the bootloader is gone. */ .section .text .global _start .type _start, @function _start: /* The bootloader has loaded us into 32-bit protected mode on a x86 machine. Interrupts are disabled. Paging is disabled. The processor state is as defined in the multiboot standard. The kernel has full control of the CPU. The kernel can only make use of hardware features and any code it provides as part of itself. There's no printf function, unless the kernel provides its own header and a printf implementation. There are no security restrictions, no safeguards, no debugging mechanisms, only what the kernel provides itself. It has absolute and complete power over the machine. */ /* To set up a stack, we set the esp register to point to the top of the stack (as it grows downwards on x86 systems). This is necessarily done in assembly as languages such as C cannot function without a stack. */ mov $stack_top, %esp /* This is a good place to initialize crucial processor state before the high-level kernel is entered. It's best to minimize the early environment where crucial features are offline. Note that the processor is not fully initialized yet: Features such as floating point instructions and instruction set extensions are not initialized yet. The GDT should be loaded here. Paging should be enabled here. C++ features such as global constructors and exceptions will require runtime support to work as well. */ /* Enter the high-level kernel. The ABI requires the stack is 16-byte aligned at the time of the call instruction (which afterwards pushes the return pointer of size 4 bytes). The stack was originally 16-byte aligned above and we've since pushed a multiple of 16 bytes to the stack since (pushed 0 bytes so far) and the alignment is thus preserved and the call is well defined. */ call kernel_main /* If the system has nothing more to do, put the computer into an infinite loop. To do that: 1) Disable interrupts with cli (clear interrupt enable in eflags). They are already disabled by the bootloader, so this is not needed. Mind that you might later enable interrupts and return from kernel_main (which is sort of nonsensical to do). 2) Wait for the next interrupt to arrive with hlt (halt instruction). Since they are disabled, this will lock up the computer. 3) Jump to the hlt instruction if it ever wakes up due to a non-maskable interrupt occurring or due to system management mode. */ cli 1: hlt jmp 1b /* Set the size of the _start symbol to the current location '.' minus its start. This is useful when debugging or when you implement call tracing. */ .size _start, . - _start ================================================ FILE: lectures/fall-2019/Supplementary-06/toyos/grub.cfg ================================================ menuentry "myos" { multiboot /boot/myos.bin } ================================================ FILE: lectures/fall-2019/Supplementary-06/toyos/kernel.c ================================================ #include #include #include /* Hardware text mode color constants. */ enum vga_color { VGA_COLOR_BLACK = 0, VGA_COLOR_BLUE = 1, VGA_COLOR_GREEN = 2, VGA_COLOR_CYAN = 3, VGA_COLOR_RED = 4, VGA_COLOR_MAGENTA = 5, VGA_COLOR_BROWN = 6, VGA_COLOR_LIGHT_GREY = 7, VGA_COLOR_DARK_GREY = 8, VGA_COLOR_LIGHT_BLUE = 9, VGA_COLOR_LIGHT_GREEN = 10, VGA_COLOR_LIGHT_CYAN = 11, VGA_COLOR_LIGHT_RED = 12, VGA_COLOR_LIGHT_MAGENTA = 13, VGA_COLOR_LIGHT_BROWN = 14, VGA_COLOR_WHITE = 15, }; static inline uint8_t vga_entry_color(enum vga_color fg, enum vga_color bg) { return fg | bg << 4; } static inline uint16_t vga_entry(unsigned char uc, uint8_t color) { return (uint16_t) uc | (uint16_t) color << 8; } size_t strlen(const char* str) { size_t len = 0; while (str[len]) len++; return len; } static const size_t VGA_WIDTH = 80; static const size_t VGA_HEIGHT = 25; size_t terminal_row; size_t terminal_column; uint8_t terminal_color; uint16_t* terminal_buffer; void terminal_initialize(void) { terminal_row = 0; terminal_column = 0; terminal_color = vga_entry_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); terminal_buffer = (uint16_t*) 0xB8000; for (size_t y = 0; y < VGA_HEIGHT; y++) { for (size_t x = 0; x < VGA_WIDTH; x++) { const size_t index = y * VGA_WIDTH + x; terminal_buffer[index] = vga_entry(' ', terminal_color); } } } void terminal_setcolor(uint8_t color) { terminal_color = color; } void terminal_putentryat(char c, uint8_t color, size_t x, size_t y) { const size_t index = y * VGA_WIDTH + x; terminal_buffer[index] = vga_entry(c, color); } void terminal_putchar(char c) { terminal_putentryat(c, terminal_color, terminal_column, terminal_row); if (++terminal_column == VGA_WIDTH) { terminal_column = 0; if (++terminal_row == VGA_HEIGHT) terminal_row = 0; } } void terminal_write(const char* data, size_t size) { for (size_t i = 0; i < size; i++) terminal_putchar(data[i]); } void terminal_writestring(const char* data) { terminal_write(data, strlen(data)); } void kernel_main(void) { /* Initialize terminal interface */ terminal_initialize(); /* Newline support is left as an exercise. */ terminal_writestring("Hello, kernel World!\n"); } ================================================ FILE: lectures/fall-2019/Supplementary-06/toyos/linker.ld ================================================ /* The bootloader will look at this image and start execution at the symbol designated as the entry point. */ ENTRY(_start) /* Tell where the various sections of the object files will be put in the final kernel image. */ SECTIONS { /* Begin putting sections at 1 MiB, a conventional place for kernels to be loaded at by the bootloader. */ . = 1M; /* First put the multiboot header, as it is required to be put very early early in the image or the bootloader won't recognize the file format. Next we'll put the .text section. */ .text BLOCK(4K) : ALIGN(4K) { *(.multiboot) *(.text) } /* Read-only data. */ .rodata BLOCK(4K) : ALIGN(4K) { *(.rodata) } /* Read-write data (initialized) */ .data BLOCK(4K) : ALIGN(4K) { *(.data) } /* Read-write data (uninitialized) and stack */ .bss BLOCK(4K) : ALIGN(4K) { *(COMMON) *(.bss) } /* The compiler may produce other sections, by default it will put them in a segment with the same name. Simply add stuff here as needed. */ } ================================================ FILE: lectures/fall-2019/Supplementary-08/custom-fd.c ================================================ #include int main() { static const char Hello1[] = "Hello, STDOUT!\n"; static const char Hello2[] = "Hello, STDERR!\n"; static const char Hello795[] = "Hello, group 795!\n"; write( 1, Hello1 , sizeof(Hello1 )-1); write( 2, Hello2 , sizeof(Hello2 )-1); write(795, Hello795, sizeof(Hello795)-1); } ================================================ FILE: lectures/fall-2019/Supplementary-10/memory-map.c ================================================ int main() { return 0; } ================================================ FILE: lectures/fall-2019/Supplementary-10/overcommit.c ================================================ #include #include int main() { int * ptr = malloc(16ULL*1024*1024*1024); // 16 Gb puts("malloc called"); getchar(); ptr[0] = 123; } ================================================ FILE: lectures/fall-2019/Supplementary-10/test-malloc.c ================================================ #include int main() { void *ptr = malloc(1); free(ptr); free(ptr); } ================================================ FILE: lectures/fall-2019/Supplementary-10/test-malloc2.c ================================================ #include int main() { void *ptr = malloc(99999999); free(ptr); free(ptr); } ================================================ FILE: lectures/fall-2019/Supplementary-11/fork-bomb.c ================================================ #include #include #include #include #include #include int main() { char * are_you_sure = getenv("ALLOW_FORK_BOMB"); if (!are_you_sure || 0!=strcmp(are_you_sure, "yes")) { fprintf(stderr, "Fork bomb not allowed!\n"); exit(127); } pid_t pid; do { pid = fork(); } while (-1 != pid); printf("Process %d reached out limit on processes\n", getpid()); while (1) { sched_yield(); sleep(1); } } ================================================ FILE: lectures/fall-2019/Supplementary-11/process_setup.c ================================================ #include #include #include #include #include #include #include int main() { pid_t pid = fork(); if (-1==pid) { perror("fork :-("); exit(1); } if (0==pid) { chdir("/usr/bin"); int fd = open("/tmp/out.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644); dup2(fd, 1); close(fd); execlp("ls", "ls", "-l", NULL); perror("exec :-("); exit(2); } else { waitpid(pid, NULL, 0); } } ================================================ FILE: lectures/fall-2019/Supplementary-11/start_child.c ================================================ #include #include #include int main() { printf("abrakadabra "); // write fflush(stdout); // pid_t result = fork(); if (0==result) { printf("I'm son\n"); // MINIX, QNX } else { printf("I'm parent\n"); } } ================================================ FILE: lectures/fall-2019/Supplementary-12/do_abort.c ================================================ #include int main() { abort(); } ================================================ FILE: lectures/fall-2019/Supplementary-12/good-signal-handling.c ================================================ #include #include #include #include volatile sig_atomic_t caught_signum = 0; void handler(int signum) { // Do not invoke printf, but just // save signal number caught_signum = signum; } int main() { struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = handler; act.sa_flags = SA_RESTART; sigaction(SIGTERM, &act, NULL); sigaction(SIGINT, &act, NULL); while (1) { pause(); // wait until signal caught and processed printf("Got signal %d\n", caught_signum); } } ================================================ FILE: lectures/fall-2019/Supplementary-12/handle-sigint-sigterm.c ================================================ #include #include #include void handler(int signum) { // Warning: govnokod! printf("Caught signal %d\n", signum); } int main() { signal(SIGINT, handler); signal(SIGTERM, handler); while (1) sched_yield(); } ================================================ FILE: lectures/fall-2019/Supplementary-12/sigaction-handling.c ================================================ #include #include #include #include void handle_with_one_arg(int signum) { // a bit better, but still govnokod printf("Got signal %d\n", signum); } void handle_with_three_args(int signum, siginfo_t *info, void *ctx) { // govnokod is still here printf("Got signal %d from process %d\n", signum, info->si_pid); } int main() { struct sigaction int_handler; memset(&int_handler, 0, sizeof(int_handler)); int_handler.sa_handler = handle_with_one_arg; int_handler.sa_flags = SA_RESTART; sigaction(SIGINT, &int_handler, NULL); struct sigaction term_handler; memset(&term_handler, 0, sizeof(term_handler)); term_handler.sa_sigaction = handle_with_three_args; term_handler.sa_flags = SA_RESTART | SA_SIGINFO; sigaction(SIGTERM, &term_handler, NULL); while (1) sched_yield(); } ================================================ FILE: lectures/fall-2019/Supplementary-12/signalfd.c ================================================ #include #include #include #include int main() { // Prepare set of signals sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); // Disable signals handling (by block) sigprocmask(SIG_BLOCK, &mask, NULL); int fd = signalfd(-1, &mask, 0); struct signalfd_siginfo info; while (read(fd, &info, sizeof(info)) > 0) { printf("Got signal %d from PID %d\n", info.ssi_signo, info.ssi_pid); } } ================================================ FILE: lectures/fall-2019/Supplementary-12/sigprocmask.c ================================================ #include #include #include volatile sig_atomic_t int_received = 0; volatile sig_atomic_t term_received = 0; void sigint_handler(int num) { int_received ++; } void sigterm_handler(int num) { term_received = 1;} int main() { // Register handlers sigaction(SIGINT, &(struct sigaction) { .sa_handler = sigint_handler, .sa_flags = SA_RESTART }, NULL); sigaction(SIGTERM, &(struct sigaction) { .sa_handler = sigterm_handler, .sa_flags = SA_RESTART }, NULL); // Block SIGINT sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); sigprocmask(SIG_BLOCK, &sigset, NULL); while ( ! term_received ) { pause(); } sigprocmask(SIG_UNBLOCK, &sigset, NULL); printf("Got %d times SIGINT\n", int_received); } ================================================ FILE: lectures/fall-2019/Supplementary-12/sigsuspend.c ================================================ #include #include #include volatile sig_atomic_t int_received = 0; void sigint_handler(int num) { int_received ++; } int main() { // Register handler sigaction(SIGINT, &(struct sigaction) { .sa_handler = sigint_handler, .sa_flags = SA_RESTART }, NULL); // Block SIGINT sigset_t set_with_int; sigemptyset(&set_with_int); sigaddset(&set_with_int, SIGINT); sigprocmask(SIG_BLOCK, &set_with_int, NULL); // Temporary unblock SIGINT and wait it sigset_t set_withOUT_int; sigfillset(&set_withOUT_int); sigdelset(&set_withOUT_int, SIGINT); sigsuspend(&set_withOUT_int); printf("Got %d times SIGINT\n", int_received); } ================================================ FILE: lectures/fall-2019/Supplementary-12/simpleio.c ================================================ #include int main() { puts("Process is sleeping until character typed"); getchar(); } ================================================ FILE: lectures/fall-2019/Supplementary-13/ldpreload-example/fakelib.c ================================================ #define _GNU_SOURCE #include #include #include #include #include typedef void (*sighandler_t)(int); typedef int (*sigaction_ptr_t) ( int, const struct sigaction *restrict, struct sigaction * ); int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *oldact) { puts("Called fake sigaction\n"); // Make additional work time to simulate // race condition sleep(3); static sigaction_ptr_t real_sigaction = NULL; if (NULL == real_sigaction) { real_sigaction = dlsym(RTLD_NEXT, "sigaction"); if (!real_sigaction) { fputs(dlerror(), stderr); _exit(1); } } return real_sigaction(signum, act, oldact); } ================================================ FILE: lectures/fall-2019/Supplementary-13/ldpreload-example/fakelib0.c ================================================ #define _GNU_SOURCE #include #include #include #include typedef void (*sighandler_t)(int); typedef int (*sigaction_ptr_t) ( int, const struct sigaction *restrict, struct sigaction * ); int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *oldact) { puts("Called fake sigaction\n"); } ================================================ FILE: lectures/fall-2019/Supplementary-13/ldpreload-example/fakelib1.c ================================================ #include __attribute__((constructor)) void initialize_fakelib() { puts("Fake library initialized"); } __attribute__((destructor)) void finalize_fakelib() { puts("Fake library unloading"); } ================================================ FILE: lectures/fall-2019/Supplementary-13/ldpreload-example/hello.c ================================================ #include int main() { puts("Hello, World!"); } ================================================ FILE: lectures/fall-2019/Supplementary-13/ldpreload-example/solution.c ================================================ #include #include #include #include void handle_sigint(int signum) { static const char Message[] = "Caught SIGINT\n"; write(1, Message, sizeof(Message)-1); } int main() { // Wrong solution: race condition printf("My PID is %d\n", getpid()); fflush(stdout); struct sigaction action_int; memset(&action_int, 0, sizeof(action_int)); action_int.sa_handler = handle_sigint; action_int.sa_flags = SA_RESTART; sigaction(SIGINT, &action_int, NULL); while (1) pause(); } ================================================ FILE: lectures/fall-2019/Supplementary-13/ptrace/ptrace_catch_string.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void premoderate_write_syscall(pid_t pid, struct user_regs_struct state) { size_t orig_buf = state.rsi; // ecx for i386 size_t size = state.rdx; // rdx for i386 char *buffer = calloc(size+sizeof(long), sizeof(*buffer)); int val = 0; for (size_t i=0; i #include #include typedef void (*sighandler_t)(int); int __real_sigaction(int signum, const struct sigaction *restrict act, struct sigaction *oldact); int __wrap_sigaction(int signum, const struct sigaction *restrict act, struct sigaction *oldact) { // Make additional work time to simulate // race condition sleep(3); return __real_sigaction(signum, act, oldact); } ================================================ FILE: lectures/fall-2019/Supplementary-13/wrap-example/solution.c ================================================ #include #include #include #include void handle_sigint(int signum) { static const char Message[] = "Caught SIGINT\n"; write(1, Message, sizeof(Message)-1); } int main() { // Wrong solution: race condition printf("My PID is %d\n", getpid()); fflush(stdout); struct sigaction action_int; memset(&action_int, 0, sizeof(action_int)); action_int.sa_handler = handle_sigint; action_int.sa_flags = SA_RESTART; sigaction(SIGINT, &action_int, NULL); while (1) pause(); } ================================================ FILE: lectures/spring-2019/Lection14-Supplementary/do_abort.c ================================================ #include int main() { abort(); } ================================================ FILE: lectures/spring-2019/Lection14-Supplementary/do_nothing.c ================================================ #include int main() { while (1) { sched_yield(); } } ================================================ FILE: lectures/spring-2019/Lection14-Supplementary/good-signal-handling.c ================================================ #include #include #include #include volatile sig_atomic_t caught_signum = 0; void handler(int signum) { // Do not invoke printf, but just // save signal number caught_signum = signum; } int main() { struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = handler; act.sa_flags = SA_RESTART; sigaction(SIGTERM, &act, NULL); sigaction(SIGINT, &act, NULL); while (1) { pause(); // wait until signal caught and processed printf("Got signal %d\n", caught_signum); } } ================================================ FILE: lectures/spring-2019/Lection14-Supplementary/handle-sigint-sigterm.c ================================================ #include #include #include void handler(int signum) { // Warning: govnokod! printf("Caught signal %d\n", signum); } int main() { signal(SIGINT, handler); signal(SIGTERM, handler); while (1) sched_yield(); } ================================================ FILE: lectures/spring-2019/Lection14-Supplementary/sigaction-handling.c ================================================ #include #include #include #include void handle_with_one_arg(int signum) { // a bit better, but still govnokod printf("Got signal %d\n", signum); } void handle_with_three_args(int signum, siginfo_t *info, void *ctx) { // govnokod is still here printf("Got signal %d from process %d\n", signum, info->si_pid); } int main() { struct sigaction int_handler; memset(&int_handler, 0, sizeof(int_handler)); int_handler.sa_handler = handle_with_one_arg; int_handler.sa_flags = SA_RESTART; sigaction(SIGINT, &int_handler, NULL); struct sigaction term_handler; memset(&term_handler, 0, sizeof(term_handler)); term_handler.sa_sigaction = handle_with_three_args; term_handler.sa_flags = SA_RESTART | SA_SIGINFO; sigaction(SIGTERM, &term_handler, NULL); while (1) sched_yield(); } ================================================ FILE: lectures/spring-2019/Lection15-Supplementary/signalfd.c ================================================ #include #include #include #include int main() { // Prepare set of signals sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); // Disable signals handling (by block) sigprocmask(SIG_BLOCK, &mask, NULL); int fd = signalfd(-1, &mask, 0); struct signalfd_siginfo info; while (read(fd, &info, sizeof(info)) > 0) { printf("Got signal %d from PID %d\n", info.ssi_signo, info.ssi_pid); } } ================================================ FILE: lectures/spring-2019/Lection15-Supplementary/sigprocmask.c ================================================ #include #include #include volatile sig_atomic_t int_received = 0; volatile sig_atomic_t term_received = 0; void sigint_handler(int num) { int_received ++; } void sigterm_handler(int num) { term_received = 1;} int main() { // Register handlers sigaction(SIGINT, &(struct sigaction) { .sa_handler = sigint_handler, .sa_flags = SA_RESTART }, NULL); sigaction(SIGTERM, &(struct sigaction) { .sa_handler = sigterm_handler, .sa_flags = SA_RESTART }, NULL); // Block SIGINT sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); sigprocmask(SIG_BLOCK, &sigset, NULL); while ( ! term_received ) { pause(); } sigprocmask(SIG_UNBLOCK, &sigset, NULL); printf("Got %d times SIGINT\n", int_received); } ================================================ FILE: lectures/spring-2019/Lection15-Supplementary/sigsuspend.c ================================================ #include #include #include volatile sig_atomic_t int_received = 0; void sigint_handler(int num) { int_received ++; } int main() { // Register handler sigaction(SIGINT, &(struct sigaction) { .sa_handler = sigint_handler, .sa_flags = SA_RESTART }, NULL); // Block SIGINT sigset_t set_with_int; sigemptyset(&set_with_int); sigaddset(&set_with_int, SIGINT); sigprocmask(SIG_BLOCK, &set_with_int, NULL); // Temporary unblock SIGINT and wait it sigset_t set_withOUT_int; sigfillset(&set_withOUT_int); sigdelset(&set_withOUT_int, SIGINT); sigsuspend(&set_withOUT_int); printf("Got %d times SIGINT\n", int_received); } ================================================ FILE: lectures/spring-2019/Lection18-Supplementary/lorem-ipsum-server.cpp ================================================ #include #include extern "C" { #include #include #include #include #include #include #include #include #include } #include #include #include static const uint16_t PortNumber = 3000; static const size_t MaxEvents = 10000; static const size_t QuantumSize = 10 /* bytes */; static const std::string Message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus elementum sem sit amet ipsum mattis, ut vestibulum diam ornare. Nullam id tortor quis arcu vehicula ornare. Nunc sit amet ultricies felis. Donec auctor sagittis commodo. Aenean cursus, turpis et imperdiet vestibulum, nibh nisi laoreet quam, a posuere ligula arcu non nisi. Sed at sodales neque, eu sollicitudin neque. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam ultricies, eros sed finibus efficitur, odio nisl volutpat ex, id tincidunt nunc metus ac elit. Sed gravida dui eu velit pellentesque, sit amet pellentesque nunc tincidunt. Cras dignissim porttitor eros vel accumsan. Sed tellus nisl, viverra quis velit laoreet, scelerisque elementum neque. Fusce purus eros, pretium sed lorem non, maximus accumsan sem. Donec tincidunt lorem eu diam ultrices, sed varius leo lobortis. Nulla nec convallis odio. Maecenas bibendum tellus vel mauris congue maximus. Proin id lectus accumsan, vehicula risus sit amet, gravida velit.\n"; static sig_atomic_t StopRequest = 0; static int create_and_bind() { int sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (0 > sd) { perror("Socket"); exit(1); } sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_addr.s_addr = INADDR_ANY; addr.sin_family = AF_INET; addr.sin_port = htons(PortNumber); int bs = bind(sd, (sockaddr*) &addr, sizeof(addr)); if (0 > bs) { perror("Bind"); close(sd); exit(1); } return sd; } static void make_non_blocking(int sd) { int flags = fcntl(sd, F_GETFL); fcntl(sd, F_SETFL, flags | O_NONBLOCK); } static void stop_handler(int s) { StopRequest = 1; } int main(int argc, char* argv[]) { int status = 0; int sock_fd = create_and_bind(); make_non_blocking(sock_fd); status = listen(sock_fd, SOMAXCONN); if (0 > status) { perror("Listen"); close(sock_fd); exit(1); } /* Create epoll queue */ int ed = epoll_create1(0); epoll_event event; memset(&event, 0, sizeof(event)); event.data.fd = sock_fd; event.events = EPOLLIN|EPOLLOUT; status = epoll_ctl(ed, EPOLL_CTL_ADD, sock_fd, &event); if (0 > status) { perror("Epoll control"); close(sock_fd); exit(1); } struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = stop_handler; act.sa_flags = SA_RESTART; sigaction(SIGINT, &act, nullptr); sigaction(SIGTERM, &act, nullptr); signal(SIGPIPE, SIG_IGN); epoll_event *pending_events = new epoll_event[MaxEvents]; std::map out_data_positions; while ( ! StopRequest ) { int n = epoll_wait(ed, pending_events, MaxEvents, -1); if (-1 == n) { break; // Bye! } else if (0 > n) { perror("Epoll wait"); close(sock_fd); exit(1); } for (int i=0; i status) { perror("Epoll control for incoming connection"); close(sock_fd); exit(1); } out_data_positions.insert(std::make_pair(incoming_fd, 0)); } continue; } /* end while(true) */ } else if ( emask & EPOLLOUT ) { // Previous data block was successfully sent, // and current connection is ready to eat some // more data int out_fd = pending_events[i].data.fd; size_t last_pos = out_data_positions[out_fd]; const std::string message_quant = Message.substr(last_pos, QuantumSize); size_t new_pos = message_quant.length() < QuantumSize ? 0 : last_pos + QuantumSize; write(out_fd, message_quant.c_str(), message_quant.length()); out_data_positions[out_fd] = new_pos; } else if ( emask & EPOLLIN ) { // Connection wants to write us some data // TODO implement me the same way! } else { std::cerr << "This branch unreachable!" << std::endl; close(sock_fd); exit(1); } } } delete[] pending_events; shutdown(sock_fd, SHUT_RDWR); close(sock_fd); std::cout << "Bye!" << std::endl; return 0; } ================================================ FILE: lectures/spring-2019/Lection20-Supplementaty/detached-threads.c ================================================ #include #include #include static void* thread_function(void* arg) { while (1) { printf("Thread %llu is running\n", (size_t)arg); sleep(1); } } int main() { const size_t N = 10; pthread_t threads[N]; for (size_t i=0; i #include #include #include #include static void* thread_function(void* arg) { while (1) { printf("Thread %"PRIu64" is running\n", (uint64_t) arg); sleep(1); } } int main() { const size_t N = 10; pthread_t threads[N]; for (size_t i=0; i #include #include #include int main(int argc, char *argv[]) { const char *shm_name = argv[1]; int fd = shm_open(shm_name, O_RDWR|O_CREAT, 0600); if (-1 == fd) { perror("SHM creation failed"); return 1; } close(fd); printf("SHM created, press ENTER to release\n"); getchar(); shm_unlink(shm_name); } ================================================ FILE: lessons-supplementary/2021-2022/l19-bpf/example1.c ================================================ #include #include #include #include #include #include #include #include #include #include int main(int argc, char *argv[]) { /* Part I. Initialization */ int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (-1==sock) { perror("socket"); exit(1); } // root or setcap CAP_NET_RAW struct ifreq req = { .ifr_name = "enp0s5" }; ioctl(sock, SIOCGIFINDEX, &req, sizeof(req)); // get 'eth0' if index struct sockaddr_ll addr = { .sll_family = AF_PACKET, // use packet-level .sll_protocol = htons(ETH_P_ALL), // accept only Ethernet .sll_ifindex = req.ifr_ifindex // device index instead of 'address' }; if (-1==bind(sock, (struct sockaddr*)&addr, sizeof(addr))) { perror("bind"); exit(1); } /* Part II. Capture packets */ for (;;) { char buffer[4096] = {}; size_t cnt = recv(sock, buffer, sizeof(buffer), 0); uint32_t from_ip, to_ip; /* Extract addresses from IPv4 header */ memcpy(&from_ip, buffer+26, sizeof(from_ip)); memcpy(&to_ip, buffer+30, sizeof(to_ip)); /* Make them human-readable */ char from_addr[20] = {}, to_addr[20] = {}; inet_ntop(AF_INET, &from_ip, from_addr, sizeof(from_addr)); inet_ntop(AF_INET, &to_ip, to_addr, sizeof(to_addr)); printf("Got communication from %s to %s\n", from_addr, to_addr); } } ================================================ FILE: lessons-supplementary/2021-2022/l19-bpf/example2.c ================================================ #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char *argv[]) { /* Part I. Initialization */ int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (-1==sock) { perror("socket"); exit(1); } struct ifreq req = { .ifr_name = "enp0s5" }; ioctl(sock, SIOCGIFINDEX, &req, sizeof(req)); // get 'eth0' if index struct sockaddr_ll addr = { .sll_family = AF_PACKET, // use packet-level .sll_protocol = htons(ETH_P_ALL), // accept only Ethernet .sll_ifindex = req.ifr_ifindex // device index instead of 'address' }; if (-1==bind(sock, (struct sockaddr*)&addr, sizeof(addr))) { perror("bind"); exit(1); } /* Attach cBPF program */ struct sock_filter code[] = { #include "filter.inc" /* some command values like: { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 5, 0x00000800 }, ........ */ }; struct sock_fprog program = { .filter = code, // program code .len = sizeof(code)/sizeof(code[0]) // instructions count }; if (0!=setsockopt( sock, // socket FD SOL_SOCKET, // the entire socket SO_ATTACH_FILTER, // 'attach filter' command &program, // .... command data sizeof(program) // .... command size )) { perror("SO_ATTACH_FILTER"); exit(1); } /* Part II. Capture packets */ for (;;) { char buffer[4096] = {}; size_t cnt = recv(sock, buffer, sizeof(buffer), 0); uint32_t from_ip, to_ip; /* Extract addresses from IPv4 header */ memcpy(&from_ip, buffer+26, sizeof(from_ip)); memcpy(&to_ip, buffer+30, sizeof(to_ip)); /* Make them human-readable */ char from_addr[20] = {}, to_addr[20] = {}; inet_ntop(AF_INET, &from_ip, from_addr, sizeof(from_addr)); inet_ntop(AF_INET, &to_ip, to_addr, sizeof(to_addr)); printf("Got communication from %s to %s\n", from_addr, to_addr); } } ================================================ FILE: lessons-supplementary/2021-2022/l19-bpf/filter.s ================================================ filter_google_dns: ldh [12] ; 16 bit Eth proto value after MACs jne #0x0800, fail ; 0x0800 = IPv4 ldb [23] ; IP header one byte proto number jne #17, fail ; 17 = UDP, 6 = TCP ld [30] ; 32 bit IPv4 address value jne #0x08080808, fail ; 8.8.8.8 success: ret #-1 ; -1 == 0xFFFFFFFF (maximum size) fail: ret #0 ; 0 is empty ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/bpf_loader.c ================================================ #include #include #include #include #include #include #include static const char License[] = "GPL"; int main() { int trace_bin_fd = open("bpf_program.bin", O_RDONLY); struct bpf_insn instructions[1000] = {}; size_t trace_bin_size = read(trace_bin_fd, instructions, sizeof(instructions)); close(trace_bin_fd); size_t instructions_count = trace_bin_size / 8; char error_log[4096] = {}; union bpf_attr bpf_argument; bpf_argument.prog_type = BPF_PROG_TYPE_XDP; bpf_argument.insns = (uint64_t) instructions; bpf_argument.insn_cnt = instructions_count; bpf_argument.license = (uint64_t) License; bpf_argument.log_level = 2; bpf_argument.log_buf = (uint64_t) error_log; bpf_argument.log_size = sizeof(error_log); int bpf_fd = syscall(SYS_bpf, BPF_PROG_LOAD, &bpf_argument, sizeof(bpf_argument)); if (-1 == bpf_fd) { perror("bpf failed"); fprintf(stderr, "Program load failed: %s\n", error_log); _exit(1); } } ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/bpf_program.c ================================================ int some_bpf_program(void *ctx) { while (1) {} return 0; } ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/call_some_func.c ================================================ #include #include void some_func() { puts("I'm some function"); } int main() { printf("Started some program with PID = %d\n", getpid()); while (1) { getchar(); // like pause some_func(); } } ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/trace_call_time.c ================================================ BPF_HASH(start_times, u64); int start_tracing(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); u64 start = bpf_ktime_get_ns(); start_times.update(&pid, &start); return 0; } int end_tracing(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); u64 *start = start_times.lookup(&pid); /*if (0 == start) { return 0; }*/ u64 now = bpf_ktime_get_ns(); u64 delta = now - *start; bpf_trace_printk("Function call time: %d", delta); return 0; } ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/trace_some_func.c ================================================ int trace_some_func(struct pt_regs *ctx) { u64 pid = bpf_get_current_pid_tgid(); bpf_trace_printk("Running traccee for PID = %d", pid); return 0; } ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/trace_syscall.c ================================================ #include int trace_execv(struct pt_regs *ctx) { char cmd[16]; bpf_get_current_comm(&cmd, sizeof(cmd)); // filter for command if (! (cmd[0]=='b' && cmd[1]=='a' && cmd[2]=='s' && cmd[3]=='h' && cmd[4]=='\0')) { return 0; } bpf_trace_printk("%s tried to exec", cmd); // bpf_override_return(ctx, 1); return 0; } ================================================ FILE: lessons-supplementary/2021-2022/l20-ebpf/trace_syscall_1.c ================================================ #include int trace_execv(struct pt_regs *ctx) { char cmd[16]; bpf_get_current_comm(&cmd, sizeof(cmd)); // filter for command if (! (cmd[0]=='b' && cmd[1]=='a' && cmd[2]=='s' && cmd[3]=='h' && cmd[4]=='\0')) { return 0; } bpf_trace_printk("%s tried to exec", cmd); bpf_override_return(ctx, 1); return 0; } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/ctor_dtor/module.c ================================================ #include void function() { puts("I'm a function into module"); } __attribute__((constructor)) static void initialize() { puts("I'm a module constructor, called once"); } __attribute__((destructor)) static void finalize() { puts("I'm a module destructor, it's cool that I'm called"); } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/ctor_dtor/run_lib.c ================================================ #include #include #include typedef void (*func_t)(); int main(int argc, char *argv[]) { const char *lib_name = argv[1]; const char *func_name = argv[2]; printf("Loaing library %s\n", lib_name); void *lib = dlopen(lib_name, RTLD_LAZY|RTLD_LOCAL); if (NULL == lib) { fprintf(stderr, "Cant load libary %s: %s\n", lib_name, dlerror()); exit(1); } printf("Loaded library %s\n", lib_name); func_t func = dlsym(lib, func_name); if (NULL == func) { fprintf(stderr, "Cant find symbol %s: %s\n", func_name, dlerror()); exit(1); } func(); printf("Unloading library %s\n", lib_name); if (0 == dlclose(lib)) { printf("Unloaded library %s\n", lib_name); } else { printf("Library %s still in use and not unloaded\n", lib_name); } } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/export_func_by_name/main.c ================================================ #include #include void cat() { puts("Meaow"); } void dog() { puts("Huff!"); } typedef void (*func_t)(); int main() { char name[4096] = {}; printf("Please enter animal kind: "); scanf("%s", name); void *myself = dlopen(NULL, RTLD_NOW|RTLD_GLOBAL); func_t func = dlsym(myself, name); func(); } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/runnable_lib/main.c ================================================ #include #include __attribute__((section(".interp"))) const char service_interp[] = "/lib/ld-linux-aarch64.so.1"; void function() { puts("I'm a function!"); } void custom_start() { function(); _exit(0); } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/use_dlopen/run_function.c ================================================ #include #include #include typedef double (*func_t)(double); int main(int argc, char* argv[]) { const char *lib_name = argv[1]; const char *func_name = argv[2]; double argument = strtod(argv[3], NULL); puts("Press Enter to load library."); getchar(); // like pause void *lib = dlopen(lib_name, RTLD_NOW|RTLD_LOCAL); if (NULL == lib) { fprintf(stderr, "Failed to load library: %s\n", dlerror()); exit(1); } else { printf("Library %s loaded at %zx. Press Enter to continue.\n", lib_name, lib); getchar(); // like pause } func_t func = dlsym(lib, func_name); if (NULL == func) { fprintf(stderr, "Failed to find function: %s\n", dlerror()); exit(1); } else { printf("Found %s in library at %zx. Press Enter to continue.\n", func_name, func); getchar(); // like pause } double value = func(argument); printf("func(%f) = %f\n", argument, value); dlclose(lib); } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/use_mmap/plugin.c ================================================ double farenheit_to_celsius(double F) { double C = (F - 32) * 5./9.; } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/use_mmap/run.c ================================================ #include #include #include #include #include #include typedef double (*func_t)(double); int main(int argc, char *argv[]) { const char *file_name = argv[1]; double argument = strtod(argv[2], NULL); int fd = open(file_name, O_RDONLY); struct stat st = {}; fstat(fd, &st); func_t func = mmap(NULL, st.st_size, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0); close(fd); if (MAP_FAILED == func) { perror("mmap failed"); exit(1); } double result = func(argument); printf("func(%f) = %f\n", argument, result); munmap(func, st.st_size); } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/use_rpath/library.c ================================================ #include void function() { puts("I'm a function from library"); } ================================================ FILE: lessons-supplementary/2021-2022/l21-libraries/use_rpath/program.c ================================================ extern void function(); int main() { function(); } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.21) project(grpc_tailbook_example) set(CMAKE_CXX_STANDARD 14) set(CMAKE_VERBOSE_MAKEFILE TRUE) find_package(PkgConfig REQUIRED) pkg_check_modules(GRPC_CXX REQUIRED grpc++) pkg_check_modules(PROTOBUF REQUIRED protobuf) include_directories(${PROTOBUF_INCLUDE_DIRS}) include_directories(${GRPC_CXX_INCLUDE_DIRS}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) set(SOURCES cplusplus/profile_server_main.cpp cplusplus/profile_service.cpp cplusplus/profile_service.h ) set(PROTO_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/social_network.pb.cc ${CMAKE_CURRENT_BINARY_DIR}/social_network.pb.h ) set(GRPC_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/social_network.grpc.pb.cc ${CMAKE_CURRENT_BINARY_DIR}/social_network.grpc.pb.h ) add_custom_command( OUTPUT ${PROTO_SOURCES} ${GRPC_SOURCES} COMMAND protoc ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} --go_out=${CMAKE_CURRENT_SOURCE_DIR}/go --python_out=${CMAKE_CURRENT_SOURCE_DIR}/python --grpc_python_out=${CMAKE_CURRENT_SOURCE_DIR}/python --dart_out=grpc:${CMAKE_CURRENT_SOURCE_DIR}/dart/lib/src/generated --grpc_out=${CMAKE_CURRENT_BINARY_DIR} --go_grpc_out=${CMAKE_CURRENT_SOURCE_DIR}/go --plugin=protoc-gen-grpc=$ENV{HOME}/bin/grpc_cpp_plugin --plugin=protoc-gen-grpc_python=$ENV{HOME}/bin/grpc_python_plugin --plugin=protoc-gen-go=$ENV{GOPATH}/bin/protoc-gen-go --plugin=protoc-gen-go_grpc=$ENV{GOPATH}/bin/protoc-gen-go-grpc --plugin=protoc-gen-dart=$ENV{HOME}/.pub-cache/bin/protoc-gen-dart -I ${CMAKE_CURRENT_SOURCE_DIR} social_network.proto DEPENDS social_network.proto ) add_executable( profile_service ${SOURCES} ${PROTO_SOURCES} ${GRPC_SOURCES} ) target_link_libraries(profile_service ${PROTOBUF_LDFLAGS} ${GRPC_CXX_LDFLAGS}) ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/cplusplus/profile_server_main.cpp ================================================ #include "profile_service.h" #include #include int main() { const std::string listen_address = "localhost:1901"; ProfileManagerService service; grpc::ServerBuilder builder; builder.AddListeningPort(listen_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr server = builder.BuildAndStart(); server->Wait(); return 0; } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/cplusplus/profile_service.cpp ================================================ #include "profile_service.h" using grpc::Status; using grpc::ServerContext; Status ProfileManagerService::GetUserProfile(ServerContext *context, const User *request, UserProfile *response) { Status status = Status::OK; if (request->login() == "vovan") { response->set_login(request->login()); response->set_first_name("Вовка"); response->set_last_name("Пу"); response->set_age(69); response->set_gender(Gender::Male); response->set_height(170.0); } else if (request->login() == "dimon") { response->set_login(request->login()); response->set_first_name("Дима"); response->set_last_name("Ме"); response->set_gender(Gender::Male); response->set_age(56); Phone* phone = response->add_phones(); phone->set_number(70001234567); phone->set_phone_type(PhoneType::Mobile); } else { status = Status(grpc::StatusCode::NOT_FOUND, "Profile not found", request->login()); } return status; } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/cplusplus/profile_service.h ================================================ #ifndef GRPC_CHAT_EXAMPLE_PROFILE_SERVICE_H #define GRPC_CHAT_EXAMPLE_PROFILE_SERVICE_H #include "social_network.grpc.pb.h" class ProfileManagerService: public ProfileManager::Service { public: grpc::Status GetUserProfile(::grpc::ServerContext *context, const ::User *request, ::UserProfile *response) override; public: }; #endif //GRPC_CHAT_EXAMPLE_PROFILE_SERVICE_H ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: c860cba910319332564e1e9d470a17074c1f2dfd channel: stable project_type: app ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/README.md ================================================ # dart A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:grpc_demo/src/main_screen.dart'; void main() { final app = MainScreen(Uri.parse('http://localhost:8081')); runApp(app); } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/lib/src/generated/social_network.pb.dart ================================================ /// // Generated code. Do not modify. // source: social_network.proto // // @dart = 2.12 // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'social_network.pbenum.dart'; export 'social_network.pbenum.dart'; class User extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'User', createEmptyInstance: create) ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'login') ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'password') ..hasRequiredFields = false ; User._() : super(); factory User({ $core.String? login, $core.String? password, }) { final _result = create(); if (login != null) { _result.login = login; } if (password != null) { _result.password = password; } return _result; } factory User.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory User.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') User clone() => User()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') User copyWith(void Function(User) updates) => super.copyWith((message) => updates(message as User)) as User; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static User create() => User._(); User createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') static User getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static User? _defaultInstance; @$pb.TagNumber(1) $core.String get login => $_getSZ(0); @$pb.TagNumber(1) set login($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) $core.bool hasLogin() => $_has(0); @$pb.TagNumber(1) void clearLogin() => clearField(1); @$pb.TagNumber(2) $core.String get password => $_getSZ(1); @$pb.TagNumber(2) set password($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasPassword() => $_has(1); @$pb.TagNumber(2) void clearPassword() => clearField(2); } class Phone extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Phone', createEmptyInstance: create) ..e(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'phoneType', $pb.PbFieldType.OE, defaultOrMaker: PhoneType.Home, valueOf: PhoneType.valueOf, enumValues: PhoneType.values) ..a<$fixnum.Int64>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'number', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; Phone._() : super(); factory Phone({ PhoneType? phoneType, $fixnum.Int64? number, }) { final _result = create(); if (phoneType != null) { _result.phoneType = phoneType; } if (number != null) { _result.number = number; } return _result; } factory Phone.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Phone.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') Phone clone() => Phone()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') Phone copyWith(void Function(Phone) updates) => super.copyWith((message) => updates(message as Phone)) as Phone; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Phone create() => Phone._(); Phone createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') static Phone getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Phone? _defaultInstance; @$pb.TagNumber(1) PhoneType get phoneType => $_getN(0); @$pb.TagNumber(1) set phoneType(PhoneType v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasPhoneType() => $_has(0); @$pb.TagNumber(1) void clearPhoneType() => clearField(1); @$pb.TagNumber(2) $fixnum.Int64 get number => $_getI64(1); @$pb.TagNumber(2) set number($fixnum.Int64 v) { $_setInt64(1, v); } @$pb.TagNumber(2) $core.bool hasNumber() => $_has(1); @$pb.TagNumber(2) void clearNumber() => clearField(2); } class UserProfile extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UserProfile', createEmptyInstance: create) ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'login') ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'firstName') ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastName') ..a<$core.int>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'age', $pb.PbFieldType.OU3) ..a<$core.double>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'height', $pb.PbFieldType.OD) ..e(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'gender', $pb.PbFieldType.OE, defaultOrMaker: Gender.Male, valueOf: Gender.valueOf, enumValues: Gender.values) ..pc(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'phones', $pb.PbFieldType.PM, subBuilder: Phone.create) ..aOS(100, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'avatarUrl') ..hasRequiredFields = false ; UserProfile._() : super(); factory UserProfile({ $core.String? login, $core.String? firstName, $core.String? lastName, $core.int? age, $core.double? height, Gender? gender, $core.Iterable? phones, $core.String? avatarUrl, }) { final _result = create(); if (login != null) { _result.login = login; } if (firstName != null) { _result.firstName = firstName; } if (lastName != null) { _result.lastName = lastName; } if (age != null) { _result.age = age; } if (height != null) { _result.height = height; } if (gender != null) { _result.gender = gender; } if (phones != null) { _result.phones.addAll(phones); } if (avatarUrl != null) { _result.avatarUrl = avatarUrl; } return _result; } factory UserProfile.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory UserProfile.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') UserProfile clone() => UserProfile()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') UserProfile copyWith(void Function(UserProfile) updates) => super.copyWith((message) => updates(message as UserProfile)) as UserProfile; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static UserProfile create() => UserProfile._(); UserProfile createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') static UserProfile getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static UserProfile? _defaultInstance; @$pb.TagNumber(1) $core.String get login => $_getSZ(0); @$pb.TagNumber(1) set login($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) $core.bool hasLogin() => $_has(0); @$pb.TagNumber(1) void clearLogin() => clearField(1); @$pb.TagNumber(2) $core.String get firstName => $_getSZ(1); @$pb.TagNumber(2) set firstName($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasFirstName() => $_has(1); @$pb.TagNumber(2) void clearFirstName() => clearField(2); @$pb.TagNumber(3) $core.String get lastName => $_getSZ(2); @$pb.TagNumber(3) set lastName($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) $core.bool hasLastName() => $_has(2); @$pb.TagNumber(3) void clearLastName() => clearField(3); @$pb.TagNumber(4) $core.int get age => $_getIZ(3); @$pb.TagNumber(4) set age($core.int v) { $_setUnsignedInt32(3, v); } @$pb.TagNumber(4) $core.bool hasAge() => $_has(3); @$pb.TagNumber(4) void clearAge() => clearField(4); @$pb.TagNumber(5) $core.double get height => $_getN(4); @$pb.TagNumber(5) set height($core.double v) { $_setDouble(4, v); } @$pb.TagNumber(5) $core.bool hasHeight() => $_has(4); @$pb.TagNumber(5) void clearHeight() => clearField(5); @$pb.TagNumber(6) Gender get gender => $_getN(5); @$pb.TagNumber(6) set gender(Gender v) { setField(6, v); } @$pb.TagNumber(6) $core.bool hasGender() => $_has(5); @$pb.TagNumber(6) void clearGender() => clearField(6); @$pb.TagNumber(10) $core.List get phones => $_getList(6); @$pb.TagNumber(100) $core.String get avatarUrl => $_getSZ(7); @$pb.TagNumber(100) set avatarUrl($core.String v) { $_setString(7, v); } @$pb.TagNumber(100) $core.bool hasAvatarUrl() => $_has(7); @$pb.TagNumber(100) void clearAvatarUrl() => clearField(100); } class Message extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Message', createEmptyInstance: create) ..aOM(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'sender', subBuilder: User.create) ..aOM(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'receiver', subBuilder: User.create) ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'content') ..hasRequiredFields = false ; Message._() : super(); factory Message({ User? sender, User? receiver, $core.String? content, }) { final _result = create(); if (sender != null) { _result.sender = sender; } if (receiver != null) { _result.receiver = receiver; } if (content != null) { _result.content = content; } return _result; } factory Message.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') Message clone() => Message()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') Message copyWith(void Function(Message) updates) => super.copyWith((message) => updates(message as Message)) as Message; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Message create() => Message._(); Message createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message? _defaultInstance; @$pb.TagNumber(1) User get sender => $_getN(0); @$pb.TagNumber(1) set sender(User v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasSender() => $_has(0); @$pb.TagNumber(1) void clearSender() => clearField(1); @$pb.TagNumber(1) User ensureSender() => $_ensure(0); @$pb.TagNumber(2) User get receiver => $_getN(1); @$pb.TagNumber(2) set receiver(User v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasReceiver() => $_has(1); @$pb.TagNumber(2) void clearReceiver() => clearField(2); @$pb.TagNumber(2) User ensureReceiver() => $_ensure(1); @$pb.TagNumber(3) $core.String get content => $_getSZ(2); @$pb.TagNumber(3) set content($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) $core.bool hasContent() => $_has(2); @$pb.TagNumber(3) void clearContent() => clearField(3); } class Empty extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Empty', createEmptyInstance: create) ..hasRequiredFields = false ; Empty._() : super(); factory Empty() => create(); factory Empty.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Empty.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') Empty clone() => Empty()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') Empty copyWith(void Function(Empty) updates) => super.copyWith((message) => updates(message as Empty)) as Empty; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Empty create() => Empty._(); Empty createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') static Empty getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Empty? _defaultInstance; } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/lib/src/generated/social_network.pbenum.dart ================================================ /// // Generated code. Do not modify. // source: social_network.proto // // @dart = 2.12 // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields // ignore_for_file: UNDEFINED_SHOWN_NAME import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; class Gender extends $pb.ProtobufEnum { static const Gender Male = Gender._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Male'); static const Gender Female = Gender._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Female'); static const $core.List values = [ Male, Female, ]; static final $core.Map<$core.int, Gender> _byValue = $pb.ProtobufEnum.initByValue(values); static Gender? valueOf($core.int value) => _byValue[value]; const Gender._($core.int v, $core.String n) : super(v, n); } class PhoneType extends $pb.ProtobufEnum { static const PhoneType Home = PhoneType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Home'); static const PhoneType Work = PhoneType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Work'); static const PhoneType Mobile = PhoneType._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Mobile'); static const $core.List values = [ Home, Work, Mobile, ]; static final $core.Map<$core.int, PhoneType> _byValue = $pb.ProtobufEnum.initByValue(values); static PhoneType? valueOf($core.int value) => _byValue[value]; const PhoneType._($core.int v, $core.String n) : super(v, n); } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/lib/src/generated/social_network.pbgrpc.dart ================================================ /// // Generated code. Do not modify. // source: social_network.proto // // @dart = 2.12 // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields import 'dart:async' as $async; import 'dart:core' as $core; import 'package:grpc/service_api.dart' as $grpc; import 'social_network.pb.dart' as $0; export 'social_network.pb.dart'; class ProfileManagerClient extends $grpc.Client { static final _$getUserProfile = $grpc.ClientMethod<$0.User, $0.UserProfile>( '/ProfileManager/GetUserProfile', ($0.User value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.UserProfile.fromBuffer(value)); ProfileManagerClient($grpc.ClientChannel channel, {$grpc.CallOptions? options, $core.Iterable<$grpc.ClientInterceptor>? interceptors}) : super(channel, options: options, interceptors: interceptors); $grpc.ResponseFuture<$0.UserProfile> getUserProfile($0.User request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$getUserProfile, request, options: options); } } abstract class ProfileManagerServiceBase extends $grpc.Service { $core.String get $name => 'ProfileManager'; ProfileManagerServiceBase() { $addMethod($grpc.ServiceMethod<$0.User, $0.UserProfile>( 'GetUserProfile', getUserProfile_Pre, false, false, ($core.List<$core.int> value) => $0.User.fromBuffer(value), ($0.UserProfile value) => value.writeToBuffer())); } $async.Future<$0.UserProfile> getUserProfile_Pre( $grpc.ServiceCall call, $async.Future<$0.User> request) async { return getUserProfile(call, await request); } $async.Future<$0.UserProfile> getUserProfile( $grpc.ServiceCall call, $0.User request); } class ChatServiceClient extends $grpc.Client { static final _$sendMessage = $grpc.ClientMethod<$0.Message, $0.Empty>( '/ChatService/SendMessage', ($0.Message value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.Empty.fromBuffer(value)); static final _$receiveMessages = $grpc.ClientMethod<$0.User, $0.Message>( '/ChatService/ReceiveMessages', ($0.User value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.Message.fromBuffer(value)); ChatServiceClient($grpc.ClientChannel channel, {$grpc.CallOptions? options, $core.Iterable<$grpc.ClientInterceptor>? interceptors}) : super(channel, options: options, interceptors: interceptors); $grpc.ResponseFuture<$0.Empty> sendMessage($0.Message request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$sendMessage, request, options: options); } $grpc.ResponseStream<$0.Message> receiveMessages($0.User request, {$grpc.CallOptions? options}) { return $createStreamingCall( _$receiveMessages, $async.Stream.fromIterable([request]), options: options); } } abstract class ChatServiceBase extends $grpc.Service { $core.String get $name => 'ChatService'; ChatServiceBase() { $addMethod($grpc.ServiceMethod<$0.Message, $0.Empty>( 'SendMessage', sendMessage_Pre, false, false, ($core.List<$core.int> value) => $0.Message.fromBuffer(value), ($0.Empty value) => value.writeToBuffer())); $addMethod($grpc.ServiceMethod<$0.User, $0.Message>( 'ReceiveMessages', receiveMessages_Pre, false, true, ($core.List<$core.int> value) => $0.User.fromBuffer(value), ($0.Message value) => value.writeToBuffer())); } $async.Future<$0.Empty> sendMessage_Pre( $grpc.ServiceCall call, $async.Future<$0.Message> request) async { return sendMessage(call, await request); } $async.Stream<$0.Message> receiveMessages_Pre( $grpc.ServiceCall call, $async.Future<$0.User> request) async* { yield* receiveMessages(call, await request); } $async.Future<$0.Empty> sendMessage( $grpc.ServiceCall call, $0.Message request); $async.Stream<$0.Message> receiveMessages( $grpc.ServiceCall call, $0.User request); } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/lib/src/generated/social_network.pbjson.dart ================================================ /// // Generated code. Do not modify. // source: social_network.proto // // @dart = 2.12 // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package import 'dart:core' as $core; import 'dart:convert' as $convert; import 'dart:typed_data' as $typed_data; @$core.Deprecated('Use genderDescriptor instead') const Gender$json = const { '1': 'Gender', '2': const [ const {'1': 'Male', '2': 0}, const {'1': 'Female', '2': 1}, ], }; /// Descriptor for `Gender`. Decode as a `google.protobuf.EnumDescriptorProto`. final $typed_data.Uint8List genderDescriptor = $convert.base64Decode('CgZHZW5kZXISCAoETWFsZRAAEgoKBkZlbWFsZRAB'); @$core.Deprecated('Use phoneTypeDescriptor instead') const PhoneType$json = const { '1': 'PhoneType', '2': const [ const {'1': 'Home', '2': 0}, const {'1': 'Work', '2': 1}, const {'1': 'Mobile', '2': 2}, ], }; /// Descriptor for `PhoneType`. Decode as a `google.protobuf.EnumDescriptorProto`. final $typed_data.Uint8List phoneTypeDescriptor = $convert.base64Decode('CglQaG9uZVR5cGUSCAoESG9tZRAAEggKBFdvcmsQARIKCgZNb2JpbGUQAg=='); @$core.Deprecated('Use userDescriptor instead') const User$json = const { '1': 'User', '2': const [ const {'1': 'login', '3': 1, '4': 1, '5': 9, '10': 'login'}, const {'1': 'password', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'password', '17': true}, ], '8': const [ const {'1': '_password'}, ], }; /// Descriptor for `User`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List userDescriptor = $convert.base64Decode('CgRVc2VyEhQKBWxvZ2luGAEgASgJUgVsb2dpbhIfCghwYXNzd29yZBgCIAEoCUgAUghwYXNzd29yZIgBAUILCglfcGFzc3dvcmQ='); @$core.Deprecated('Use phoneDescriptor instead') const Phone$json = const { '1': 'Phone', '2': const [ const {'1': 'phone_type', '3': 1, '4': 1, '5': 14, '6': '.PhoneType', '10': 'phoneType'}, const {'1': 'number', '3': 2, '4': 1, '5': 4, '10': 'number'}, ], }; /// Descriptor for `Phone`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List phoneDescriptor = $convert.base64Decode('CgVQaG9uZRIpCgpwaG9uZV90eXBlGAEgASgOMgouUGhvbmVUeXBlUglwaG9uZVR5cGUSFgoGbnVtYmVyGAIgASgEUgZudW1iZXI='); @$core.Deprecated('Use userProfileDescriptor instead') const UserProfile$json = const { '1': 'UserProfile', '2': const [ const {'1': 'login', '3': 1, '4': 1, '5': 9, '10': 'login'}, const {'1': 'first_name', '3': 2, '4': 1, '5': 9, '10': 'firstName'}, const {'1': 'last_name', '3': 3, '4': 1, '5': 9, '10': 'lastName'}, const {'1': 'age', '3': 4, '4': 1, '5': 13, '10': 'age'}, const {'1': 'height', '3': 5, '4': 1, '5': 1, '10': 'height'}, const {'1': 'gender', '3': 6, '4': 1, '5': 14, '6': '.Gender', '10': 'gender'}, const {'1': 'phones', '3': 10, '4': 3, '5': 11, '6': '.Phone', '10': 'phones'}, const {'1': 'avatar_url', '3': 100, '4': 1, '5': 9, '9': 0, '10': 'avatarUrl', '17': true}, ], '8': const [ const {'1': '_avatar_url'}, ], }; /// Descriptor for `UserProfile`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List userProfileDescriptor = $convert.base64Decode('CgtVc2VyUHJvZmlsZRIUCgVsb2dpbhgBIAEoCVIFbG9naW4SHQoKZmlyc3RfbmFtZRgCIAEoCVIJZmlyc3ROYW1lEhsKCWxhc3RfbmFtZRgDIAEoCVIIbGFzdE5hbWUSEAoDYWdlGAQgASgNUgNhZ2USFgoGaGVpZ2h0GAUgASgBUgZoZWlnaHQSHwoGZ2VuZGVyGAYgASgOMgcuR2VuZGVyUgZnZW5kZXISHgoGcGhvbmVzGAogAygLMgYuUGhvbmVSBnBob25lcxIiCgphdmF0YXJfdXJsGGQgASgJSABSCWF2YXRhclVybIgBAUINCgtfYXZhdGFyX3VybA=='); @$core.Deprecated('Use messageDescriptor instead') const Message$json = const { '1': 'Message', '2': const [ const {'1': 'sender', '3': 1, '4': 1, '5': 11, '6': '.User', '10': 'sender'}, const {'1': 'receiver', '3': 2, '4': 1, '5': 11, '6': '.User', '10': 'receiver'}, const {'1': 'content', '3': 3, '4': 1, '5': 9, '10': 'content'}, ], }; /// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List messageDescriptor = $convert.base64Decode('CgdNZXNzYWdlEh0KBnNlbmRlchgBIAEoCzIFLlVzZXJSBnNlbmRlchIhCghyZWNlaXZlchgCIAEoCzIFLlVzZXJSCHJlY2VpdmVyEhgKB2NvbnRlbnQYAyABKAlSB2NvbnRlbnQ='); @$core.Deprecated('Use emptyDescriptor instead') const Empty$json = const { '1': 'Empty', }; /// Descriptor for `Empty`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List emptyDescriptor = $convert.base64Decode('CgVFbXB0eQ=='); ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/lib/src/main_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:grpc/grpc.dart'; import 'package:grpc/grpc_connection_interface.dart'; import 'package:grpc/grpc_or_grpcweb.dart'; import 'package:grpc/grpc_web.dart'; import 'package:grpc_demo/src/generated/social_network.pb.dart'; import 'package:grpc_demo/src/generated/social_network.pbgrpc.dart'; class MainScreen extends StatefulWidget { final Uri grpcLocation; const MainScreen(this.grpcLocation, {Key? key}) : super(key: key); @override State createState() => MainScreenState(); } class MainScreenState extends State { final TextEditingController _loginName = TextEditingController(); UserProfile? _loadedProfile; String? _errorText; late final ProfileManagerClient _profileManagerApi; @override void initState() { super.initState(); final grpcChannel = GrpcWebClientChannel.xhr(widget.grpcLocation); _profileManagerApi = ProfileManagerClient(grpcChannel); } void loadUserProfile() { String login = _loginName.text; final futureProfile = _profileManagerApi.getUserProfile(User(login: login)); futureProfile.then((UserProfile value) { setState((){ _errorText = null; _loadedProfile = value; }); }) .onError((error, stackTrace) { setState(() { _loadedProfile = null; _errorText = error.toString() + '\n' + stackTrace.toString(); }); }); } @override Widget build(BuildContext context) { List items = []; final searchBox = Card( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 450, height: 64, child: TextField( controller: _loginName, onEditingComplete: loadUserProfile, ) ), ElevatedButton( onPressed: loadUserProfile, child: const Icon(Icons.search), ), ] ), ); items.add(searchBox); if (_loadedProfile != null) { items.add(buildProfile(context)); } if (_errorText != null) { items.add(Text(_errorText!, style: TextStyle( color: Theme.of(context).errorColor, ), )); } return MaterialApp( title: 'gRPC Demo', home: Scaffold( appBar: AppBar( title: const Text('gRPC Demo'), centerTitle: true, ), body: Column(children: items), ), ); } Widget buildProfile(BuildContext context) { final theme = Theme.of(context); List items = []; void addRowItem(String label, String value) { items.add(Row( children: [ Text(label + ': ', style: TextStyle(color: theme.primaryColor), textAlign: TextAlign.right, ), Text(value), ], )); } addRowItem('First name', _loadedProfile!.firstName); addRowItem('Last name', _loadedProfile!.lastName); addRowItem('Age', '${_loadedProfile!.age} years'); return Card( child: Column(children: items), ); } } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/pubspec.yaml ================================================ name: grpc_demo description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: sdk: ">=2.16.2 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: grpc: ^3.0.2 flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^1.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/web/index.html ================================================ dart ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/dart/web/manifest.json ================================================ { "name": "dart", "short_name": "dart", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/go/chat_server_main.go ================================================ package main import ( pb "chat_server/generated/chat_server" "google.golang.org/grpc" "log" "net" ) func main() { service := ChatService{} service.Initialize() var opts []grpc.ServerOption grpcServer := grpc.NewServer(opts...) pb.RegisterChatServiceServer(grpcServer, &service) listener, err := net.Listen("tcp", "localhost:1902") if err != nil { log.Fatalf("Cant listen: %v", err) } grpcServer.Serve(listener) } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/go/chat_service.go ================================================ package main import ( pb "chat_server/generated/chat_server" "context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type ChatService struct { pb.UnimplementedChatServiceServer Inboxes map[string]chan *pb.Message } func (service *ChatService) Initialize() { service.Inboxes = make(map[string]chan *pb.Message) } func (service *ChatService) SendMessage(ctx context.Context, message *pb.Message) (*pb.Empty, error) { receiver := message.Receiver.Login inbox, exists := service.Inboxes[receiver] if !exists { return nil, status.Error(codes.NotFound, "User is not present online") } inbox <- message return &pb.Empty{}, nil } func (service *ChatService) ReceiveMessages(user *pb.User, sink pb.ChatService_ReceiveMessagesServer) error { inbox, exists := service.Inboxes[user.Login] if !exists { service.Inboxes[user.Login] = make(chan *pb.Message) inbox = service.Inboxes[user.Login] } else { inbox = service.Inboxes[user.Login] } for message := range inbox { if err := sink.Send(message); err != nil { break } } return nil } ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/go/go.mod ================================================ module chat_server go 1.18 require ( google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.26.0 ) require ( github.com/golang/protobuf v1.5.2 // indirect golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect golang.org/x/text v0.3.0 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect ) replace chat_server v0.0.0 => ./generated/chat_server ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/python/main.py ================================================ import asyncio import grpc import social_network_pb2_grpc as gen import social_network_pb2 as pb HOST = 'localhost' PROFILES_PORT = 1901 CHAT_PORT = 1902 profiles_server = grpc.insecure_channel(f'{HOST}:{PROFILES_PORT}') profile_manager = gen.ProfileManagerStub(profiles_server) chat_server = grpc.insecure_channel(f'{HOST}:{CHAT_PORT}') chat_service = gen.ChatServiceStub(chat_server) def get_profile(name: str) -> pb.UserProfile: user = pb.User(login=name) return profile_manager.GetUserProfile(user) def send_message(sender: str, receiver: str, text: str): from_user = pb.User(login=sender) to_user = pb.User(login=receiver) message = pb.Message(sender=from_user, receiver=to_user, content=text) chat_service.SendMessage(message) def start_receiving(login: str): user = pb.User(login=login) messages_stream = chat_service.ReceiveMessages(user) for message in messages_stream: print(f'-- Got message from {message.sender.login}: {message.content}') ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/python/social_network_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: social_network.proto """Generated protocol buffer code.""" from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14social_network.proto\"9\n\x04User\x12\r\n\x05login\x18\x01 \x01(\t\x12\x15\n\x08password\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_password\"7\n\x05Phone\x12\x1e\n\nphone_type\x18\x01 \x01(\x0e\x32\n.PhoneType\x12\x0e\n\x06number\x18\x02 \x01(\x04\"\xb9\x01\n\x0bUserProfile\x12\r\n\x05login\x18\x01 \x01(\t\x12\x12\n\nfirst_name\x18\x02 \x01(\t\x12\x11\n\tlast_name\x18\x03 \x01(\t\x12\x0b\n\x03\x61ge\x18\x04 \x01(\r\x12\x0e\n\x06height\x18\x05 \x01(\x01\x12\x17\n\x06gender\x18\x06 \x01(\x0e\x32\x07.Gender\x12\x16\n\x06phones\x18\n \x03(\x0b\x32\x06.Phone\x12\x17\n\navatar_url\x18\x64 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_avatar_url\"J\n\x07Message\x12\x15\n\x06sender\x18\x01 \x01(\x0b\x32\x05.User\x12\x17\n\x08receiver\x18\x02 \x01(\x0b\x32\x05.User\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\"\x07\n\x05\x45mpty*\x1e\n\x06Gender\x12\x08\n\x04Male\x10\x00\x12\n\n\x06\x46\x65male\x10\x01*+\n\tPhoneType\x12\x08\n\x04Home\x10\x00\x12\x08\n\x04Work\x10\x01\x12\n\n\x06Mobile\x10\x02\x32\x37\n\x0eProfileManager\x12%\n\x0eGetUserProfile\x12\x05.User\x1a\x0c.UserProfile2T\n\x0b\x43hatService\x12\x1f\n\x0bSendMessage\x12\x08.Message\x1a\x06.Empty\x12$\n\x0fReceiveMessages\x12\x05.User\x1a\x08.Message0\x01\x42\x17Z\x15generated/chat_serverb\x06proto3') _GENDER = DESCRIPTOR.enum_types_by_name['Gender'] Gender = enum_type_wrapper.EnumTypeWrapper(_GENDER) _PHONETYPE = DESCRIPTOR.enum_types_by_name['PhoneType'] PhoneType = enum_type_wrapper.EnumTypeWrapper(_PHONETYPE) Male = 0 Female = 1 Home = 0 Work = 1 Mobile = 2 _USER = DESCRIPTOR.message_types_by_name['User'] _PHONE = DESCRIPTOR.message_types_by_name['Phone'] _USERPROFILE = DESCRIPTOR.message_types_by_name['UserProfile'] _MESSAGE = DESCRIPTOR.message_types_by_name['Message'] _EMPTY = DESCRIPTOR.message_types_by_name['Empty'] User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), { 'DESCRIPTOR' : _USER, '__module__' : 'social_network_pb2' # @@protoc_insertion_point(class_scope:User) }) _sym_db.RegisterMessage(User) Phone = _reflection.GeneratedProtocolMessageType('Phone', (_message.Message,), { 'DESCRIPTOR' : _PHONE, '__module__' : 'social_network_pb2' # @@protoc_insertion_point(class_scope:Phone) }) _sym_db.RegisterMessage(Phone) UserProfile = _reflection.GeneratedProtocolMessageType('UserProfile', (_message.Message,), { 'DESCRIPTOR' : _USERPROFILE, '__module__' : 'social_network_pb2' # @@protoc_insertion_point(class_scope:UserProfile) }) _sym_db.RegisterMessage(UserProfile) Message = _reflection.GeneratedProtocolMessageType('Message', (_message.Message,), { 'DESCRIPTOR' : _MESSAGE, '__module__' : 'social_network_pb2' # @@protoc_insertion_point(class_scope:Message) }) _sym_db.RegisterMessage(Message) Empty = _reflection.GeneratedProtocolMessageType('Empty', (_message.Message,), { 'DESCRIPTOR' : _EMPTY, '__module__' : 'social_network_pb2' # @@protoc_insertion_point(class_scope:Empty) }) _sym_db.RegisterMessage(Empty) _PROFILEMANAGER = DESCRIPTOR.services_by_name['ProfileManager'] _CHATSERVICE = DESCRIPTOR.services_by_name['ChatService'] if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'Z\025generated/chat_server' _GENDER._serialized_start=413 _GENDER._serialized_end=443 _PHONETYPE._serialized_start=445 _PHONETYPE._serialized_end=488 _USER._serialized_start=24 _USER._serialized_end=81 _PHONE._serialized_start=83 _PHONE._serialized_end=138 _USERPROFILE._serialized_start=141 _USERPROFILE._serialized_end=326 _MESSAGE._serialized_start=328 _MESSAGE._serialized_end=402 _EMPTY._serialized_start=404 _EMPTY._serialized_end=411 _PROFILEMANAGER._serialized_start=490 _PROFILEMANAGER._serialized_end=545 _CHATSERVICE._serialized_start=547 _CHATSERVICE._serialized_end=631 # @@protoc_insertion_point(module_scope) ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/python/social_network_pb2_grpc.py ================================================ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import social_network_pb2 as social__network__pb2 class ProfileManagerStub(object): """Missing associated documentation comment in .proto file.""" def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.GetUserProfile = channel.unary_unary( '/ProfileManager/GetUserProfile', request_serializer=social__network__pb2.User.SerializeToString, response_deserializer=social__network__pb2.UserProfile.FromString, ) class ProfileManagerServicer(object): """Missing associated documentation comment in .proto file.""" def GetUserProfile(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def add_ProfileManagerServicer_to_server(servicer, server): rpc_method_handlers = { 'GetUserProfile': grpc.unary_unary_rpc_method_handler( servicer.GetUserProfile, request_deserializer=social__network__pb2.User.FromString, response_serializer=social__network__pb2.UserProfile.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'ProfileManager', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class ProfileManager(object): """Missing associated documentation comment in .proto file.""" @staticmethod def GetUserProfile(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/ProfileManager/GetUserProfile', social__network__pb2.User.SerializeToString, social__network__pb2.UserProfile.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) class ChatServiceStub(object): """Missing associated documentation comment in .proto file.""" def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.SendMessage = channel.unary_unary( '/ChatService/SendMessage', request_serializer=social__network__pb2.Message.SerializeToString, response_deserializer=social__network__pb2.Empty.FromString, ) self.ReceiveMessages = channel.unary_stream( '/ChatService/ReceiveMessages', request_serializer=social__network__pb2.User.SerializeToString, response_deserializer=social__network__pb2.Message.FromString, ) class ChatServiceServicer(object): """Missing associated documentation comment in .proto file.""" def SendMessage(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def ReceiveMessages(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def add_ChatServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'SendMessage': grpc.unary_unary_rpc_method_handler( servicer.SendMessage, request_deserializer=social__network__pb2.Message.FromString, response_serializer=social__network__pb2.Empty.SerializeToString, ), 'ReceiveMessages': grpc.unary_stream_rpc_method_handler( servicer.ReceiveMessages, request_deserializer=social__network__pb2.User.FromString, response_serializer=social__network__pb2.Message.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'ChatService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class ChatService(object): """Missing associated documentation comment in .proto file.""" @staticmethod def SendMessage(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/ChatService/SendMessage', social__network__pb2.Message.SerializeToString, social__network__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ReceiveMessages(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_stream(request, target, '/ChatService/ReceiveMessages', social__network__pb2.User.SerializeToString, social__network__pb2.Message.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) ================================================ FILE: lessons-supplementary/2021-2022/l23-grpc/social_network.proto ================================================ syntax = "proto3"; // Part I - implemented by C++ server message User { string login = 1; optional string password = 2; } enum Gender { Male = 0; Female = 1; } enum PhoneType { Home = 0; Work = 1; Mobile = 2; } message Phone { PhoneType phone_type = 1; uint64 number = 2; } message UserProfile { string login = 1; string first_name = 2; string last_name = 3; uint32 age = 4; double height = 5; Gender gender = 6; repeated Phone phones = 10; optional string avatar_url = 100; } service ProfileManager { rpc GetUserProfile(User) returns (UserProfile); } // Part II - implemented by GoLang server option go_package = "generated/chat_server"; message Message { User sender = 1; User receiver = 2; string content = 3; } message Empty {} service ChatService { rpc SendMessage(Message) returns (Empty); rpc ReceiveMessages(User) returns (stream Message); } ================================================ FILE: lessons-supplementary/2021-2022/l24-kernel-fs/fuse/fusepy-memory-example.py ================================================ #!/usr/bin/env python from __future__ import print_function, absolute_import, division import logging from collections import defaultdict from errno import ENOENT from stat import S_IFDIR, S_IFLNK, S_IFREG from sys import argv, exit from time import time from fuse import FUSE, FuseOSError, Operations, LoggingMixIn if not hasattr(__builtins__, 'bytes'): bytes = str class Memory(LoggingMixIn, Operations): 'Example memory filesystem. Supports only one level of files.' def __init__(self): self.files = {} self.data = defaultdict(bytes) self.fd = 0 now = time() self.files['/'] = dict(st_mode=(S_IFDIR | 0o755), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2) def chmod(self, path, mode): self.files[path]['st_mode'] &= 0o770000 self.files[path]['st_mode'] |= mode return 0 def chown(self, path, uid, gid): self.files[path]['st_uid'] = uid self.files[path]['st_gid'] = gid def create(self, path, mode): self.files[path] = dict(st_mode=(S_IFREG | mode), st_nlink=1, st_size=0, st_ctime=time(), st_mtime=time(), st_atime=time()) self.fd += 1 return self.fd def getattr(self, path, fh=None): if path not in self.files: raise FuseOSError(ENOENT) return self.files[path] def getxattr(self, path, name, position=0): attrs = self.files[path].get('attrs', {}) try: return attrs[name] except KeyError: return '' # Should return ENOATTR def listxattr(self, path): attrs = self.files[path].get('attrs', {}) return attrs.keys() def mkdir(self, path, mode): self.files[path] = dict(st_mode=(S_IFDIR | mode), st_nlink=2, st_size=0, st_ctime=time(), st_mtime=time(), st_atime=time()) self.files['/']['st_nlink'] += 1 def open(self, path, flags): self.fd += 1 return self.fd def read(self, path, size, offset, fh): return self.data[path][offset:offset + size] def readdir(self, path, fh): return ['.', '..'] + [x[1:] for x in self.files if x != '/'] def readlink(self, path): return self.data[path] def removexattr(self, path, name): attrs = self.files[path].get('attrs', {}) try: del attrs[name] except KeyError: pass # Should return ENOATTR def rename(self, old, new): self.files[new] = self.files.pop(old) def rmdir(self, path): self.files.pop(path) self.files['/']['st_nlink'] -= 1 def setxattr(self, path, name, value, options, position=0): # Ignore options attrs = self.files[path].setdefault('attrs', {}) attrs[name] = value def statfs(self, path): return dict(f_bsize=512, f_blocks=4096, f_bavail=2048) def symlink(self, target, source): self.files[target] = dict(st_mode=(S_IFLNK | 0o777), st_nlink=1, st_size=len(source)) self.data[target] = source def truncate(self, path, length, fh=None): self.data[path] = self.data[path][:length] self.files[path]['st_size'] = length def unlink(self, path): self.files.pop(path) def utimens(self, path, times=None): now = time() atime, mtime = times if times else (now, now) self.files[path]['st_atime'] = atime self.files[path]['st_mtime'] = mtime def write(self, path, data, offset, fh): self.data[path] = self.data[path][:offset] + data self.files[path]['st_size'] = len(self.data[path]) return len(data) if __name__ == '__main__': if len(argv) != 2: print('usage: %s ' % argv[0]) exit(1) logging.basicConfig(level=logging.DEBUG) fuse = FUSE(Memory(), argv[1], foreground=True) ================================================ FILE: lessons-supplementary/2021-2022/l24-kernel-fs/modules/Makefile ================================================ obj-m += hello.o hello-with-param.o all: make -C /lib/modules/`uname -r`/build M=$(PWD) V=1 modules clean: make -C /lib/modules/`uname -r`/build M=$(PWD) V=1 clean ================================================ FILE: lessons-supplementary/2021-2022/l24-kernel-fs/modules/hello-with-param.c ================================================ #include // modules macros #include #include // basic kernel routines int some_flag_value = 0; module_param(some_flag_value, int, 0600); int init_module() { if (some_flag_value) { printk("Module created with some non-zero flag\n"); } else { printk("Module created with zero flag\n"); } return 0; // on success } void cleanup_module() { printk("Unloading kernel module 'hello'\n"); } MODULE_LICENSE("GPL"); ================================================ FILE: lessons-supplementary/2021-2022/l24-kernel-fs/modules/hello.c ================================================ #include // modules macros #include // basic kernel routines int init_module() { printk("Hello from newly created module!\n"); return 0; // on success } void cleanup_module() { printk("Unloading kernel module 'hello'\n"); } MODULE_LICENSE("My Great License to use only in Russia"); ================================================ FILE: lessons-supplementary/2021-2022/l24-kernel-fs/modules/hello2.c ================================================ #include // modules macros #include // basic kernel routines int init_module() { printk("Hello from newly created module!\n"); return 0; // on success } void cleanup_module() { printk("Unloading kernel module 'hello'\n"); } MODULE_LICENSE("My Great License to use only in Russia"); ================================================ FILE: practice/.clang-format ================================================ --- Language: Cpp AccessModifierOffset: -2 AlignAfterOpenBracket: AlwaysBreak AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlines: Right AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: None AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: MultiLine BinPackArguments: false BinPackParameters: false BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: true AfterNamespace: false AfterObjCDeclaration: false AfterStruct: false AfterUnion: false AfterExternBlock: false BeforeCatch: false BeforeElse: false IndentBraces: false SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true BreakBeforeBinaryOperators: None BreakBeforeBraces: Linux BreakBeforeInheritanceComma: false BreakInheritanceList: BeforeColon BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: BeforeColon BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true ColumnLimit: 80 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: true IncludeBlocks: Preserve IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 - Regex: '^(<|"(gtest|gmock|isl|json)/)' Priority: 3 - Regex: '.*' Priority: 1 IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: false IndentPPDirectives: None IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: true MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None ObjCBinPackProtocolList: Auto ObjCBlockIndentWidth: 2 ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 19 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Left ReflowComments: false SortIncludes: true SortUsingDeclarations: true SpaceAfterCStyleCast: false SpaceAfterTemplateKeyword: true SpaceBeforeAssignmentOperators: true SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp11 StatementMacros: - Q_UNUSED - QT_REQUIRE_VERSION TabWidth: 4 UseTab: Never ... ================================================ FILE: practice/aarch64/README.md ================================================ # Архитектура AArch64 (armv8) ## Кросс-компиляция и запуск программ на x86 Процесс сборки программ, предназначенных для другой процессорной архитектуры или операционной системы называется кросс-компиляцией. Для этого необходимо специальная версия компилятора `gcc`, предназначенного для другой платформы. Во многих дистрибутивах существуют отдельные пакеты компилятора для других платформ, включая различные варианты ARM. Готовую сборку компилятора для armv8 можно взять из проекта Linaro: [http://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/aarch64-linux-gnu/](http://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/aarch64-linux-gnu/). Полные названия команд `gcc` имеют вид *триплетов*: ``` ARCH-OS[-VENDOR]-gcc ARCH-OS[-VENDOR]-g++ ARCH-OS[-VENDOR]-gdb и т. д. ``` где `ARCH` - это имя архитектуры: `i686`, `x86_64`, `arm`, `aarch64`, `ppc` и т.д.; `OS` - целевая операционная система, например `linux`, `win32` или `darwin`; а необязательный фрагмент триплета `VENDOR` - соглашения по бинарному интерфейсу, если их для платформы существует несколько, например для ARM это может быть `gnu` (стандартное соглашение Linux) или `none` (без операционной системы, просто голое железо). Выполнение программ, предназначенных для других архитектур, возможно только интерпретацией инородного набора команд. Для этого предназначены специальные программы - эмуляторы. Архитектуры arm/aarch64, как и многие другие архитектуры, поддерживает эмулятор QEMU. Эмулировать можно как компьютерную систему целиком, по аналогии с VirtualBox, так и только набор команд процессора, используя при этом окружение хост-системы Linux. Команды QEMU имеют вид: ``` qemu-ARCH qemu-system-ARCH ``` где ARCH - это имя эмулируемой архитектуры. Команды, в названии которых присутствует system, запускают эмуляцию компьютерной системы, и для их использования необходимо установить операционную систему. Команды без system требуют в качестве обязательного аргумента имя выполняемого файла для ОС Linux, и эмулируют только набор команд процессора в пользовательском режиме, выполняя "инородный" исполняемых файл так, как будто это обычная программа. Поскольку большинство программ, скомпилированных для Linux и другой процессорной архитектуры,, подразумевают использование стандартной библиотеки Си, необходимо использовать именно версию glibc для для нужной архитектуры. Минимальное окружение с необходимыми библиотеками можно взять из проекта Linaro (см. ссылку выше), и скормить его qemu с помощью опции -L ПУТЬ_К_SYSROOT. Пример компиляции и запуска: ``` # в предположении, что компилятор распакован в /opt/aarch64-gcc, # а sysroot - в /opt/aarch64-sysroot # Компилируем > /opt/aarch64-gcc/bin/aarch64-linux-gnu-gcc -o program hello.c # На выходе получаем исполняемый файл, который не запустится > ./program bash: ./program: cannot execute binary file: Exec format error # Но мы можем запустить его с помощью qemu-aarch64 > qemu-aarch64 -L /opt/aarch64-sysroot ./program Hello, World! ``` У команд `qemu-*` предусмотрена возможность запуска в режиме отладки, - в этом случае возможно взаимодействие с `qemu` точно так же, как и с `gdbserver`: ``` # Компилируем с отладочной информацией > /opt/aarch64-gcc/bin/aarch64-linux-gnu-gcc -g -o program hello.c # Запускаем под qemu в режиме отладки # Обратите внимание на опцию -g ПОРТ > qemu-aarch64 -L /opt/aarch64-sysroot -g 1234 ./program # В другом терминале можно подключиться к программе из gdb > /opt/aarch64-gcc/bin/aarch64-linux-gnu-gdb ./program (gdb) target remote localhost 1234 ``` ## Программирование на языке ассемблера armv8 Программы на языка ассемблера для компилятора GNU сохраняются в файле, имя которого оканчивается на `.s` или `.S`. Во втором случае (с заглавной буквой) подразумевается, что текст программы может быть обработан препроцессором, то есть можно использовать конструкции `#define` или `#include`. Для компиляции используется одна из команд: `aarch64-linux-gnu-as` или `aarch64-linux-gnu-gcc`. В первом случае текст только компилируется в объектный файл, во втором - в выполняемую программу, скомпонованную со стандартной библиотекой Си, из которой можно использовать функции ввода-вывода. Полная документация по архитектуре команд armv8 приведена в [официальном источнике](https://developer.arm.com/documentation/ddi0487/latest). Описание основных инструкций приведено в разделе *C3: A64 Instruction Set Overview*. ### Общий синтаксис программ на ассемблере Разберем синтаксис ассемблера armv8 на примере реализации функции `f`, которая доступна извне, и вычисляет выражение `A*x*x + B*x + C`, где `A`, `B`, `C` и `x` являются агументами функции `f(A,B,C,x)`. ``` // Это комментарий, как в Си/С++ .text // начало секции .text с кодом программы .global f // указание о том, что метка f будет доступна извне // (аналог extern в языке Си) f: // метка - имя функции или строки для перехода // последовательность команд для вычисления mul x0, x0, x3 mul x0, x0, x3 mul x1, x1, x3 add x0, x0, x1 add x0, x0, x2 // возвращаемся из функции f ret ``` ### Целочисленные регистры Процессор может выполнять операции только над регистрами - ячейками памяти в ядре процессора. У armv8 есть 31 регистр общего назначения, доступных программно: `x0`, `x1`, ... , `x30`. Размер каждого регистра - 64 бит. Обращение по именам `w0`, `w1`, ..., `w30` к этим регистрам означает использование младших 32-битных частей соответствующих регистров с префиксом `x`. Регистры `x30` и `sp` следует изменять с осторожностью, поскольку у них есть специальное назначения для всех платформ armv8, независимо от используемой операционной системы: `x30` хранит адрес возврата из функции, `sp` - указатель на вершину стека. Кроме того, в системе Linux предусмотрены следующие соглашения об использовании регистров: | Регистры | Назначение | | --------------- | ------------------------------------------------------------ | | `x0` ... `x7` | аргументы функции и возвращаемое значение (`x0`) | | `x8` ... `x18` | временные регистры, для которые не гарантируется сохранение результата, если вызывать какую-либо функцию | | `x19` ... `x28` | регистры, для которых гарантируется, что вызываемая функция их не будет портить | | `x29` | указатель на границу фрейма функции, обычно используется отладчиком | | `x30` | адрес возврата из функции | | `sp` | указатель на вершину стека | Помимо регистров общего назначения предусмотрены еще два специальных регистра: `xzr` - всегда хранит значение `0`, и `pc` - Program Counter, который хранит адрес следующей инструкции, которая должна быть выполнена. ### Флаги Выполнение команд может приводить к появлению некоторой дополнительной информации, которая хранится в *регистре флагов*. Флаги относятся к последней выполненной команде. Основные флаги, это: - `C`: Carry - возникло беззнаковое переполнение - `V`: oVerflow - возникло знаковое переполнение - `N`: Negative - отрицательный результат - `Z`: Zero - обнуление результата. ### Команды процессора Команды процессора выполняются определенные действия над регистрами. Некоторые команды, которые называются командами переходов, позволяют менять значение регистра `pc`. Для арифметических команд необходимо указывать в качестве первого аргумента регистр, в который нужно записать результат, а остальные аргументы - это аргументы операции. Если используются регистры с префиксом `w`, то подразумевается 32-битная арифметика, если регистры с префиксом `x` - 64-битная арифметика. Для того, чтобы арифметические команды изменяли флаги, необходимо указать им суффикс `s`, - в противном случае значения флагов не изменятся. #### Арифметические и поразрядные операции Базовые арифметические команды: * `add Xd, Xa, Xb` // Xd ← Xa + Xb * `sub Xd, Xa, Xb` // Xd ← Xa - Xb * `mul Xd, Xa, Xb` // Xd ← Xa * Xb * `madd Xd, Xa, Xb, Xc` // Xd ← Xa * Xb + Xc * `umaddl Xd, Wa, Wb, Xc` // Xd ← Wa * Wb + Xc, где Wa и Wb - беззнаковые значения * `smaddl Xd, Wa, Wb, Xc` // Xd ← Wa * Wb + Xc, где Wa и Wb - знаковые значения * `udiv Xd, Xa, Xb` // Xd ← Xa / Xb, где Xa и Xb - беззнаковые значения * `sdiv Xd, Xa, Xb` // Xd ← Xa / Xb, где Xa и Xb - знаковые значения Побитовые операции: * `and Xd, Xa, Xb` // Xd ← Xa & Xb * `orr Xd, Xa, Xb` // Xd ← Xa | Xb * `eor Xd, Xa, Xb` // Xd ← Xa ^ Xb * `asr Xd, Xa, Xb` // Xd ← Xa >> Xb (арифметический сдвиг, деление на степени 2) * `lsr Xd, Xa, Xb` // Xd ← Xa >> Xb (логический сдвиг) * `lsl Xd, Xa, Xb` // Xd ← Xa << Xb (логический сдвиг) Копирование и приведения типов: * `mov Xd, Xa` // Xd ← Xa * `uxtb Xd, Xa` // Xd ← Xa, где Xa - это uint8_t * `uxth Xd, Xa` // Xd ← Xa, где Xa - это uint16_t * `uxtw Xd, Xa` // Xd ← Xa, где Xa - это uint32_t * `sxtb Xd, Xa` // Xd ← Xa, где Xa - это int8_t * `sxth Xd, Xa` // Xd ← Xa, где Xa - это int16_t * `sxtw Xd, Xa` // Xd ← Xa, где Xa - это int32_t ### Команды управления ходом программы Внутри программы отдельные инструкции могут иметь метки, - именованные относительные адреса в программе, которые ассемблер при сборке программы заменяет на численные смещения относительно текущей инструкции. Управление ходом программы осуществляется инструкциями перехода на метки, которые могут быть как безусловными, так и выполняться при выполнении определенного условия. #### Безусловный переход на другую инструкцию Команды безусловного перехода: * `b LABEL` - безусловный переход на метку `LABEL`, которая закодирована в команду как смещение относительно текущей выполняемой инструкции, эта инструкция обычно используется для переходов внутри функции; * `bl LABEL` - безусловный переход на метку `LABEL`, как в случае инструкции `b`, но при этом в регистр `x30` сохраняется адрес следующей за `bl` инструкции, чтобы к нему можно было вернуться инструкцией `ret`; обычно эта инструкция используется для вызова функций, метки которых синтаксически ничем не отличаются от меток внутри функций. Метки должны быть отдалены от текущей инструкции не более чем на `±32Mb`. Этого обычно более чем достаточно для вызова функции из того же самого исполняемого файла, где используется вызов функции, но возможны и вызовы функций, которые в памяти располагаются значительно дальше, например функции библиотек. Для таких случаев предусмотрена возможность перехода на инструкцию по 64-битному адресу: * `br X` - безусловный переход на адрес, который хранится в регистре `X`; * `brl X` - безусловный переход на адрес, который хранится в регистре `X`, с сохранением адреса возврата в регистре `x30` , который может быть использован инструкцией `ret`. #### Условные переходы Арифметические операции, у которых в названии указан суффикс `s`, а также команда сравнения регистров `cmp Xa, Xb` изменяют набор флагов процессора. Эти флаги могут использоваться в качестиве условий для операций условного перехода, которые кодируются в виде суффиксов инструкции `b`: ``` EQ equal (Z) NE not equal (!Z) CS or HS carry set / unsigned higher or same (C) CC or LO carry clear / unsigned lower (!C) MI minus / negative (N) PL plus / positive or zero (!N) VS overflow set (V) VC overflow clear (!V) HI unsigned higher (C && !Z) LS unsigned lower or same (!C || Z) GE signed greater than or equal (N == V) LT signed less than (N != V) GT signed greater than (!Z && (N == V)) LE signed less than or equal (Z || (N != V)) ``` Пример. Реализация цикла, который в Си нотации эквивалентен `for (x0=0; x0= x1 // какие-то инструкции внутри цикла add x0, x0, 1 // инкремент переменной цикла b LoopBegin // переходим к началу цикла, где будет проверка LoopEnd: // цикл закончился ``` ### Взаимодействие с памятью В традиционной для RISC-архитектур модели адресации, процессоры умеют выполнять действия только над регистрами. Любое взаимодействие с памятью осуществляется отдельными командами загрузки и сохранения. Для базового обращения к памяти используются инструкции: * `ldr Rd, [Xa]` - прочитать из памяти содержимое по адресу, указанному в регистре `Xa` и сохранить результат в `Rd` * `str Ra, [Xd]` - сохранить содержимое регистра `Ra` в памяти по адресу, указанному в регистре `Xd` Эти инструкции оперируют 32 или 64-битными данными (размерность определяется названием регистра). Для чтения/записи операндов меньшего размера эти инструкции имеют дополнительные суффиксы: * `ldrb`/`strb` - для `uint8_t` * `ldrsb`/`strsb` - для `int8_t` * `ldrh`/`strh` - для `uint16_t` * `ldrsh`/`strsh` - для `int16_t` * `ldrsw`/`strsw` - для `int32_t` Обратите внимание на то, что для меньшей, чем размер регистра, разрядности используются разные инструкции для знаковых и беззнаковых типов данных. Это связано с тем, как должен обрабатываться старший (знаковый) бит целого числа, - либо оставаться на месте, либо становиться старшим битом регистра. Для  64-битной архитектуры ARMv8 (но не для 32-битных архитектур ARM) возможно указание смещения относительно базового адреса. Этот синтаксис может быть использован для обращения к полям структур, если известен адрес начала структуры в памяти, либо для обращения к элементам массивов по индексу. ``` // загрузка значения по адресу из x1 со смещением 8 байт ldr x0, [x1, 8] // x0 = *(x1 + 8) // загрузка значения по адресу из x1 с индексом элемента x2, // в предположении, что размер одного элемента равен 8 байтам ldr x0, [x1, x2, lsl 3] // x0 = *(x1 + x2 * (1 << 3)) ``` ================================================ FILE: practice/aarch64-functions/README.md ================================================ # Стек и вызов функций ## Функции и метки Все метки, за исключением меток, начинающихся с префикса `.L`, попадают в таблицу символов, которую можно посмотреть командой `objdump -t`. Метки могут быть как локальными, то есть предназначенными только для использования в пределах объектного файла, так и глобальными, - доступными извне. Чтобы сделать метку глобальной, необходимо использовать директиву `.global` в исходном тексте на языке ассемблера. С точки зрения внутреннего представления, функции и метки - это одни и тоже. Переход на метку осуществляется инструкцией `b`, возможно, с каким-то суффиксом-условием. Для вызова функций используется инструкция `bl`, которая выполняет следующие действия: 1. Сохраняет в регистр `x30` значение `pc + 4` 2. Выполняет переход на метку, указанную в инструкции. Регистр `x30` при этом имеет специальное назначение - Link Register, в некоторых источниках его еще называют `lr`, хотя компилятор `gcc` явно это имя не поддерживает для архитектуры `aarh64`. Смысл этого регистра в том, что он хранит адрес возврата из функции. ## Соглашение о вызовах функций в Linux/AArch64 Набор регистров процессора существует только в единственном экземпляре, поэтому при вызове функций все регистры используются повторно, и это необходимо учитывать. Для определенных групп регистров, по соглашению, принятому в Linux (и большинстве других систем), принято следующее функциональное назначение: * Регистры с `x0` до `x7` включительно используются для передачи аргументов в функцию. Кроме того, регистр `x0` используется для вовзрата значения. * Регистр `x8` используется в качестве указателя `this` для объектно-ориентированных языков программирования. * Регистры  c `x9` по `x15` используются как временные регистры. * Регистры `x16` и `x17` используются как временные регистры для вычисления адреса функции во время прыжка из секции `.plt`. * Регистр `x18` используется как указатель на хранилище Thread-local переменных. * Регистры с `x19` по `x28` должны быть сохранены, если они используются функцией. * Регистр `x29` используется как Frame Pointer, используемый для быстрого разворачиваения стека вызовов. * Регистр `x30` - это Link Register, используемый инструкцией `ret`. * Регистр `x31` (для `gcc` его имя `sp`) - это указатель на вершину стека. В случае вызова функции **не гарантируется**, что не будут изменены значения регистров с `x0` по `x18` включительно, поскольку реализация функции вправе использовать эти регистру по своему усмотрению. Для регистров с `x19` по `x31` гарантируется, что их значения не изменятся в случае вызова функции. В случае, если какой-либо функции требуется использование этих регистров, то функция обязана восстановить их исходные значения перед вызовом инструкции `ret`. ## Стек вызовов Для хранения локальный значений и сохранения регистров с `x19` по `x30` может использоваться оперативная память в специальной области, которая называется *стек*. Память для стека имеет фиксированный размер (для Linux по умолчанию это обычно 8Мб), и она выделена перед началом выполнения программы. Данные в стек помещаются сверху вниз, а указатель на нижнюю границу стека для каждой функции хранится в регистре `sp`. Кроме того, для архитектуры `aarch64` (независимо от используемой операционной системы) требуется, чтобы стек был выровнен по границе в 16 байт, в противном случае обращение к стеку приведет к ошибке Bus Error. Пример. Выделение памяти на стеке и сохранение регистра `lr`: ```assembly function: // выделяем память на стеке, просто перемещая указатель sp // обратите внимание на то, что изменять стек можно на число, // кратное 16, даже если требуется меньше памяти sub sp, sp, 16 // сохраняем на стек регистр x30 // можно сохранять как в самый низ стека [sp], // так и со смещением в 8 байт [sp, 8] str x30, [sp, 8] // .... какой-то код с использованием инструкции bl // перед выходом восстанавливаем из стека x30 ldr x30, [sp, 8] // вовзращаем значение sp в исходное add sp, sp, 16 // теперь можно выходить ret ``` ================================================ FILE: practice/arm/README.md ================================================ # Разработка под архитектуру ARM ## Кросс-компиляция Процесс сборки программ, предназначенных для другой процессорной архитектуры или операционной системы называется кросс-компиляцией. Для этого необходимо специальная версия компилятора `gcc`, предназначенного для другой платформы. Во многих дистрибутивах существуют отдельные пакеты компилятора для других платформ, включая ARM. Кроме того, для архитектуры ARM можно скачать готовую поставку "все-в-одном" из проекта Linaro: [http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/arm-linux-gnueabi/](http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/arm-linux-gnueabi/). Полные названия команд `gcc` имеют вид *триплетов*: ``` ARCH-OS[-VENDOR]-gcc ARCH-OS[-VENDOR]-g++ ARCH-OS[-VENDOR]-gdb и т. д. ``` где `ARCH` - это имя архитектуры: `i686`, `x86_64`, `arm`, `ppc` и т.д.; `OS` - целевая операционная система, например `linux`, `win32` или `darwin`; а необязательный фрагмент триплета `VENDOR` - соглашения по бинарному интерфейсу, если их для платформы существует несколько, например для ARM это может быть `gnueabi` (стандартное соглашение Linux) или `none-eabi` (без операционной системы, просто голое железо). Для ARM ещё часто различают название архитектуры на `arm` (soft float) и `armhf` (hard float). В первом случае подразумевается отсутствие блока с плавающей точкой, поэтому все операции эмулируются программно, во втором случае - выполняются аппаратно. ## Выполнение программ для не родных архитектур Выполнение программ, предназначенных для других архитектур, возможно только интерпретацией инородного набора команд. Для этого предназначены специальные программы - *эмуляторы*. Архитектуру ARM, как и многие другие архитектуры, поддерживает эмулятор [QEMU](https://www.qemu.org/). Эмулировать можно как компьютерную систему целиком, по аналогии с VirtualBox, так и только набор команд процессора, используя при этом окружение хост-системы Linux. ### Запуск бинарников ARM в родном окружении Этот эмулятор входит в состав всех распространенных дистрибутивов. Команды qemu имеют вид: ``` qemu-ARCH qemu-system-ARCH ``` где `ARCH` - это имя эмулируемой архитектуры. Команды, в названии которых присутствует `system`, запускают эмуляцию компьютерной системы, и для их использования необходимо установить операционную систему. Команды без `system` требуют в качестве обязательного аргумента имя выполняемого файла для ОС Linux, и эмулируют только набор команд процессора в *пользовательском режиме*, выполняя "инородный" исполняемых файл так, как будто это обычная программа. Поскольку большинство программ, скомпилированных для ARM Linux, подразумевают использование стандартной библиотеки Си, необходимо использовать именно версию glibc для ARM. Минимальное окружение с необходимыми библиотеками можно взять из проекта Linaro (см. ссылку выше), и скормить его qemu с помощью опции `-L ПУТЬ_К_SYSROOT`. Пример компиляции и запуска: ``` # в предположении, что компилятор распакован в /opt/arm-gcc, # а sysroot - в /opt/arm-sysroot # Компилируем > /opt/arm-gcc/bin/arm-linux-gnueabi-gcc -marm -o program hello.c # На выходе получаем исполняемый файл, который не запустится > ./program bash: ./program: cannot execute binary file: Exec format error # Но мы можем запустить его с помощью qemu-arm > qemu-arm -L /opt/arm-sysroot ./program Hello, World! ``` ### Запуск ARM-программ в эмуляции окружения Raspberry Pi Идеальный вариант для тестирования и отладки - это использовать настоящее железо, например Raspberry Pi. Если под рукой нет компьютера с ARM-процессором, то можно выполнять эмуляцию ПК с установленной системой Raspbian. Скачать образ можно отсюда: [гуглодиск](https://drive.google.com/open?id=11lc_f-_crhP-CJi_FEYb4DE0u9TMViT4) ================================================ FILE: practice/arm_globals_plt/README.md ================================================ # Адресация данных в памяти и использование библиотечных функций * [Reference по ARM](../asm/arm_basics/arm_reference.pdf) ## Основные команды Как свойственно классической RISC-архитектуре, процессор ARM может выполнять операции только над регистрами. Для доступа к памяти используются отдельные команды *загрузки* (`ldr`) и *сохранения* (`str`). Общий вид команд: ``` LDR{условие}{тип} Регистр, Адрес STR{условие]{тип} Регистр, Адрес ``` где `{условие}` - это условие выполнения команды, может быть пустым (см. предыдущий семинар); `{тип}` - тип данных: * `B` - беззнаковый байт * `SB` - знаковый байт * `H` - полуслово (16 бит) * `SB`- знаковое полуслово * `D` - двойное слово. Если тип в названии команды не указан, то подразумевается обычное слово. Обратите внимание, что для выполнения операций загрузки/сохранения данных, меньших, чем машинное слово, отдельно выделяются знаковые команды, которые делают аккуратное расширение бит нулями, сохраняя при этом старший знаковый бит. В случае операций загрузки/сохранения пары регистров (двойное слово), регистр должен быть с четным номером. Второе машинное слово подразумевается в соседнем регистре с номером `Rn+1`. ## Адресация Адрес имеет вид: `[R_base {, offset}]` где `R_base` - имя регистра, который содержит базовый адрес в памяти, а необязательный параметр `offset` - смещение относительно адреса. Итоговый адрес определяется как `*R_base + offset`. Смещение может быть как именем регистра, так и численной константой, закодированной в команду. Регистры обычно используются для индексации элементов массива, константы - для доступа к полям структуры или локальным переменным и аргументам относительно `[sp]`. ## Адресация полей Си-структур По стандарту языка Си, поля в памяти структур размещаются по следующим правилам: * порядок полей в памяти соответствует порядку полей в описании структуры * размер структуры должен быть кратен размеру машинного слова * данные внутри машинных слов размещаются таким образом, чтобы быть прижатыми к их границам. Таким образом, размер структуры не всегда совпадает с суммой размеров отдельных полей. Например: ``` struct A { char f1; // 1 байт int f2; // 4 байта char f3; // 1 байт }; // 1 + 4 + 1 = 6 байт // size(struct A) = 12 байт ``` В данном примере поле `f1` занимает часть машинного слова, поле `f2` - имеет размер 4 байта, поэтому занимает уже следующее машинное слово, и для поля `f3` приходится использовать ещё одно. Простая перестановка полей местами позволяет сэкономить 4 байта: ``` struct A { char f1; // 1 байт char f3; // 1 байт int f2; // 4 байта }; // 1 + 1 + 4 = 6 байт // size(struct A) = 8 байт ``` В этом случае поля `f1` и `f3` занимают одно и то же машинное слово. Компилятор GCC имеет нестандартный аттрибут `packed`, позволяющий создавать "упакованные" структуры, размер которых равен сумме размеров отдельных его полей: ``` struct A { char f1; // 1 байт int f2; // 4 байта char f3; // 1 байт } __attribute__((packed)); // 1 + 4 + 1 = 6 байт // size(struct A) = 6 байт ``` ## Функции стандартной Си-библиотеки С каждой функцией, которую можно использовать извне, связана некоторая текстовая метка в таблице символов. После компиляции, запись в таблице символов определяет место в памяти, где размещается первая инструкция функции. Функции, реализованные в разных объектных модулях, но компонуемые в один исполняемый файл, вызываются обычным образом. Способ их вызова ничем не отличается от вызова функций из одного и того же объектного модуля. При использовании *библиотек*, они загружаются в отдельную область памяти, и на этапе компоновки адрес размещения библиотек не известен. Более того, размещение самой программы, в общем случае, также предполагается неизвестным. Такие функции, которые находятся в динамически загружаемых бибилиотеках, включая стандартную библиотеку Си, отображаются в таблице символов с пометкой `@plt`. Их реализация выглядит на языке ассемблера примерно следующим образом: ``` function@plt: // Во временный регистр IP загружаем текущий PC // с некоторым смещением. По этому смещению находится // таблица адресов реальных функций, которая заполняется // на этапе загрузки программы и динамических библиотек add ip, pc, #0 add ip, ip, #OFFSET_TO_TABLE_BEGIN // Загружаем значение адреса из этой таблицы в PC. // Это приводит к тому, что переходим к выполнению // реальной функции. ldr pc, [ip, #OFFSET_TO_FUNCTION_INDEX] ``` Таким образом, функции из внешних библиотек разполагаются как бы в самой программе, но представляют собой "трамплин" для выполнения реальной функций. ================================================ FILE: practice/asm/arm_basics/README.md ================================================ # Основы ассемблера ARM ## Написание и компиляция программ Программы на языка ассемблера для компилятора GNU сохраняются в файле, имя которого оканчивается на `.s` или `.S`. Во втором случае (с заглавной буквой) подразумевается, что текст программы может быть обработан препроцессором. Для компиляции используется одна из команд: `arm-linux-gnueabi-as` или `arm-linux-gnueabi-gcc`. В первом случае текст только компилируется в объектный файл, во втором - в выполняемую программу, скомпонованную со стандартной библиотекой Си, из которой можно использовать функции ввода-вывода. Процессоры ARM поддерживают два набора команд: основной 32-битный `arm`, и уплотнённый 16-битный `thumb`, между которыми процессор умеет переключаться. В рамках данного семинара мы будем использовать 32-битный набор инструкций, поэтму тексты нужно компилировать с опцией `-marm`. ## Общий синтакис ``` // Это комментарий, как в C++ .text // начало секции .text с кодом программы .global f // указание о том, что метка f // является доступной извне (аналог extern) f: // метка (заканчивается двоеточием) // последовательность команд mul r0, r0, r3 mul r0, r0, r3 mul r1, r1, r3 add r0, r0, r1 add r0, r0, r2 mov r1, r0 bx lr ``` ## Регистры Процессор может выполнять операции только над *регистрами* - 32-биьными ячейками памяти в ядре процессора. У ARM есть 16 регистров, доступных программно: `r0`, `r1`, ... ,`r15`. У регистров `r13`...`r15` имеются специальные назначения и дополнительные имена: * `r15` = `pc`: Program Counter - указатель на текущую выполняемую инструкцию * `r14` = `lr`: Link Register - хранит адрес возврата из функции * `r13` = `sp`: Stack Pointer - указатель на вершину стека. ## Флаги Выполнение команд может приводить к появлению некоторой дополнительной информации, которая хранится в *регистре флагов*. Флаги относятся к последней выполненной команде. Основные флаги, это: * `C`: Carry - возникло беззнаковое переполнение * `V`: oVerflow - возникло знаковое переполнение * `N`: Negative - отрицательный результат * `Z`: Zero - обнуление результата. ## Команды Полный перечень 32-битных команд см. в [этом reference](arm_reference.pdf), начиная со 151 страницы. Архитектура ARM-32 подразумевает, что почти все команды могут иметь *условное выполнение*. Условие кодируется 4-мя битами в самой команде, а с точки зрения синтаксиса ассемблера у команд могут быть суффиксы. Таким образом, каждая команда состоит из двух частей (без разделения пробелами): сама команда и её суффикс. ## Базовые арифметические операции * `AND regd, rega, argb` // regd ← rega & argb * `EOR regd, rega, argb` // regd ← rega ^ argb * `SUB regd, rega, argb` // regd ← rega − argb * `RSB regd, rega, argb` // regd ← argb - rega * `ADD regd, rega, argb` // regd ← rega + argb * `ADC regd, rega, argb` // regd ← rega + argb + carry * `SBC regd, rega, argb` // regd ← rega − argb − !carry * `RSC regd, rega, argb` // regd ← argb − rega − !carry * `TST rega, argb` // set flags for rega & argb * `TEQ rega, argb` // set flags for rega ^ argb * `CMP rega, argb` // set flags for rega − argb * `CMN rega, argb` // set flags for rega + argb * `ORR regd, rega, argb` // regd ← rega | argb * `MOV regd, arg` // regd ← arg * `BIC regd, rega, argb` // regd ← rega & ~argb * `MVN regd, arg` // regd ← ~argb ## Суффиксы-условия ``` EQ equal (Z) NE not equal (!Z) CS or HS carry set / unsigned higher or same (C) CC or LO carry clear / unsigned lower (!C) MI minus / negative (N) PL plus / positive or zero (!N) VS overflow set (V) VC overflow clear (!V) HI unsigned higher (C && !Z) LS unsigned lower or same (!C || Z) GE signed greater than or equal (N == V) LT signed less than (N != V) GT signed greater than (!Z && (N == V)) LE signed less than or equal (Z || (N != V)) ``` ## Переходы Счетчик `pc` автоматически увеличивается на 4 при выполнении очередной инструкции. Для ветвления программ изпользуются команды: * `B label` - переход на метку; используется внутри функций для ветвлений, связанных с циклами или условиями * `BL label` - сохранение текущего `pc` в `lr` и переход на `label`; обычно используется для вызова функций * `BX register` - переход к адресу, указанному в регистре; обычно используется для выхода из функций. ## Работа с памятью Процессор может выполнять операции только над регистрами. Для взаимодействия с памятью используются отдельные инструкции загрузки/сохранения регистров. * `LDR regd, [regaddr]` - загружает машинное слово из памяти по адресу, хранящимся в regaddr, и сохраняет его в регистре regd * `STR reds, [regaddr]` - сохраняет в памяти машинное слово из регистра regs по адресу, указанному в регистре regaddr. ================================================ FILE: practice/asm/arm_load_store/README.md ================================================ # Адресация данных в памяти ## Дригие материалы семинара * [Reference по ARM](../arm_basics/arm_reference.pdf) * [Лекция по IEEE754](../../../lectures/fall-2018/Lection03-InstrEncoding_IEEE754.pdf) ## Основные команды Как свойственно классической RISC-архитектуре, процессор ARM может выполнять операции только над регистрами. Для доступа к памяти используются отдельные команды *загрузки* (`ldr`) и *сохранения* (`str`). Общий вид команд: ``` LDR{условие}{тип} Регистр, Адрес STR{условие]{тип} Регистр, Адрес ``` где `{условие}` - это условие выполнения команды, может быть пустым (см. предыдущий семинар); `{тип}` - тип данных: * `B` - беззнаковый байт * `SB` - знаковый байт * `H` - полуслово (16 бит) * `SB`- знаковое полуслово * `D` - двойное слово. Если тип в названии команды не указан, то подразумевается обычное слово. Обратите внимание, что для выполнения операций загрузки/сохранения данных, меньших, чем машинное слово, отдельно выделяются знаковые команды, которые делают аккуратное расширение бит нулями, сохраняя при этом старший знаковый бит. В случае операций загрузки/сохранения пары регистров (двойное слово), регистр должен быть с четным номером. Второе машинное слово подразумевается в соседнем регистре с номером `Rn+1`. ## Адресация Адрес имеет вид: `[R_base {, offset}]` где `R_base` - имя регистра, который содержит базовый адрес в памяти, а необязательный параметр `offset` - смещение относительно адреса. Итоговый адрес определяется как `*R_base + offset`. Смещение может быть как именем регистра, так и численной константой, закодированной в команду. Регистры обычно используются для индексации элементов массива, константы - для доступа к полям структуры или локальным переменным и аргументам относительно `[sp]`. ## Адресация полей Си-структур По стандарту языка Си, поля в памяти структур размещаются по следующим правилам: * порядок полей в памяти соответствует порядку полей в описании структуры * размер структуры должен быть кратен размеру машинного слова * данные внутри машинных слов размещаются таким образом, чтобы быть прижатыми к их границам. Таким образом, размер структуры не всегда совпадает с суммой размеров отдельных полей. Например: ``` struct A { char f1; // 1 байт int f2; // 4 байта char f3; // 1 байт }; // 1 + 4 + 1 = 6 байт // size(struct A) = 12 байт ``` В данном примере поле `f1` занимает часть машинного слова, поле `f2` - имеет размер 4 байта, поэтому занимает уже следующее машинное слово, и для поля `f3` приходится использовать ещё одно. Простая перестановка полей местами позволяет сэкономить 4 байта: ``` struct A { char f1; // 1 байт char f3; // 1 байт int f2; // 4 байта }; // 1 + 1 + 4 = 6 байт // size(struct A) = 8 байт ``` В этом случае поля `f1` и `f3` занимают одно и то же машинное слово. Компилятор GCC имеет нестандартный аттрибут `packed`, позволяющий создавать "упакованные" структуры, размер которых равен сумме размеров отдельных его полей: ``` struct A { char f1; // 1 байт int f2; // 4 байта char f3; // 1 байт } __attribute__((packed)); // 1 + 4 + 1 = 6 байт // size(struct A) = 6 байт ``` ================================================ FILE: practice/asm/nostdlib_baremetal/README.md ================================================ # Жизнь без стандартной библиотеки Основной reference по набору команд [преобразованный в HTML](https://www.felixcloutier.com/x86/). ## Инструменты для сборки без стандартной библиотеки ### Инструменты GNU При компоновке с опцией `-nostdlib` линковщик не включает функцию `main`, и не связывает программу со стандартной библиотекой языка Си. Получаемый на выходе файл - обычный выполняемый файл в формате ELF, который можно выполнить в операционной системе. Размещение различных секций файла при компоновке можно указать в специальном ld-файле (подробнее см. [LD: Scripts](https://sourceware.org/binutils/docs/ld/Scripts.html)), который указывается опцией `-T имя_файла`. Для того, чтобы при компоновке не включалась лишняя информация о том, каким компилятором собрана программа, исползуется опция линковщика `--build-id=none`. Для выделения кода самой программы из ELF-файла можно использовать утилиту `objcopy`. ### Ассемблер NASM Ассемблер `nasm` использует хоть и похожий на Intel, но всё же немного отличающийся по синтаксису язык. Этот ассемблер, в отличии от GNU, поддерживает много выходных форматов, в том числе flat-файлы, предназначенные для непосредственной заливки программатором или загрузки в память. ## Взаимодействие с внешним миром в Linux #### Общие сведения о системных вызовах Системные вызовы - это функции, реализованные в ядре операционной системы, и поэтому обычные процессы могут вызывать их только используя специальные команды, которые переключают процессор в режим ядра. Для доступа к системным вызовам используются нестандартные способы вызова: либо механизм прерываний (команда `int`), либо специализированная команда архитектуры x86-64 `syscall`. Для большинства (но не для всех) системных вызовов реализованы Си-сигнатуры, которые описаны во 2-м разделе man-страниц. Поскольку соглашения о вызовах обычных Си-функций отличаются от соглашений о системных вызовах, стандартная библиотека языка Си содержит короткие функции-оболочки, единственная задача которых - это переложить аргументы в соотвествии с требуемым соглашением, после чего выполнить системный вызов, и вернуть результат. Примеры некоторых системных вызовов в Linux: * `exit` (`_exit` в Си-нотации) = `1` - выход из программы; * `read` = `3` - чтение из файлового дескриптора; * `write` = `4` - запись в файловый дескриптор; * `brk` (`sbrk` в Си-нотации) = `45` - перемещение границы сегмента данных программы. Для обращения к произвольному системному вызову по его номеру, например, если для него не реализована функция-оболочка в стандартной Си-библиотеке, используется функция `syscall`: ```c #include #include int main() { const char Hello[] = "Hello!\n"; // эквивалентно вызову // write(1, Hello, sizeof(Hello)-1); syscall(SYS_write, 1, Hello, sizeof(Hello)-1); } ``` #### 32-разрядные системы x86 Операционная система Linux реализует системные вызовы через программное прерывание с номером `0x80`, которое можно инициировать командой `int`. В регистре `eax` хранится номер системного вызова, в регистрах `ebx`, `ecx`, `edx`, `esi`, `edi` передаются аргументы, а возвращаемое значение передается через `eax`. Номера системных вызовов на x86 перечислены в файле `/usr/include/asm/unistd_32.h`. Пример для x86 (вывод строки `Hello` с использованием системного вызова `write`): ```asm .text ...... mov eax, 4 // 4 - номер write mov ebx, 1 // 1 - файловый дескриптор stdout mov ecx, hello_ptr // указатель на hello mov edx, 5 // количество байт в выводе int 0x80 // системный вызов Linux ...... .data hello: .string "Hello" hello_ptr: .long hello ``` #### 64-разрядные системы x86-64 В 64-битных системах возможно использовать соглашения о системных вызовах для 32-битных платформ x86, но этот механизм используется исключительно для обеспечения работоспособности старых 32-битных программ. При использовании инструкции `int 0x80` аргументы, передаваемые через регистры, усекаюстся до 32-битных значений, что может приводить к неопределенному поведению, например, если передаются указатели. ``` c // переменная хранится на стеке, поэтому ее адрес // имеет достаточно большое значение в виртуальном // 64-разрядном адресном пространстве процесса char buffer[1024]; // если использовать int 0x80, значение указателя buffer // будет записано в 32-битный регистр ecx, что приведет к // ошибке Segmentation Fault ssize_t bytes_read = read(0, buffer, sizeof(buffer)); ``` Родным для архитектуры x86-64 соглашением в Linux является использование команды процессора `syscall`, где номер системного вызова передается через `rax`, а аргументы передаются через регистры: `rdi`, `rsi`, `rdx`, `r10`, `r8` и `r9`. Обратите внимание, что не все используемые регистры совпадают со стандартным соглашением о вызовах в x86-64, например, вместо регистра `rcx` используется регистр `r10`. Кроме того, использование команды `syscall` может испортить содержимое регистров `rcx` и `r11`. Номера системных вызовов для использования их командой `syscall`, хранятся в заголовочном файле `/usr/include/sys/syscall.h`, и большинство из них совпадают (хотя это ничем не гарантируется) с номерами системных вызовов для 32-битных системных вызовов архитектуры x86. Пример для x86-64 (вывод строки `Hello` с использованием системного вызова `write`): ```asm .text ...... mov rax, 4 // 4 - номер write mov rdi, 1 // 1 - файловый дескриптор stdout mov rsi, hello_ptr // указатель на hello mov rdx, 5 // количество байт в выводе syscall // системный вызов Linux ...... .data hello: .string "Hello" hello_ptr: .quad hello ``` ## Взаимодействие с внешним миром через BIOS или в DOS (историческая справка) До момента загрузки операционной системы, обработка ввода-вывода осуществляется с помощью подпрограмм, предоставляемых BIOS (Basic Input Output System). Разные подсистемам ("сервисам") соответствуют различные номера прерываний. Например, прерывание `0x10` предназначено для вывода на экран, а прерывание `0x09` - за чтение с клавиатуры. Некоторые операционные системы, например DOS, не запрещают использование прерываний BIOS, а дополняют их своими механизмами. Подробное описание функций BIOS и DOS - [здесь](http://www.codenet.ru/progr/dos/). Отдельно стоит рассмотреть взаимодействие с выводом на экран. Поскольку вывод через прерывание является хоть и универсальным, но все же медленным способом, то лучше использовать прямую запись в видеопамять VGA. Видеопамять VGA в архитектуре x86 располагается в диапазоне `0xA000...0xDFFFF` (256Кб начиная с 640Кб), и делится на "окна", - области, назначение которых зависит от используемого [режима работы](https://wiki.osdev.org/VGA_Hardware#Memory_Layout_in_text_modes). В стандартном текстовом видеорежиме, вывод символа в позицию `(X, Y)` осуществляется записью двух байт по адресу `0xB8000+Y*80*2+X*2`, где младший байт означает код символа, а старший - цвет символа и фона. ## Стадии загрузки системы ### Включение компьютера Сразу после запуска компьютера, управление передаётся программе из ROM-памяти (часто именуемую BIOS, хотя это не совсем корректно), задача которой - выполнить диагностику системы, определить конфигурацию оборудования, и загрузить программу-загрузчик с определенного диска, чтобы передать ей управление. Программа-загрузчик может располагаться: * в классической PC-системе - в первых 512 байтах диска; * в современных системах с EFI/UEFI - выделяется определенная область в Flash-памяти на системной плате, куда установщик операционной системы записывает свой загрузчик. ### Загрузка системы через MBR Master Boot Record имеет размер 512 байт, и состоит из двух частей: программы-загрузчика и первичной таблицы разделов диска. Признаком того, что MBR имеет загрузчик, является значение `0x55AA` в последних двух байтах. Размер первичной таблицы разделов для PC - 64 байта, таким образом, для загрузчика остается всего 446 байт (512-2-64). Если загрузчик является достаточно сложным (например, GRUB в графическом режиме со всякими красивостями и умной командной строкой), то его делят на две части: в MBR и частично - на разделе диска. Первые 446 байт загружаются с диска в память по адресу `0x7C00`, а область памяти от `0x0000` до `0x7C00` считается зарезервированной под стек. При этом, процессор x86 работает в 16-битном реальном режиме, со старинной сегментной адресацией памяти. Пример программирования MBR - [здесь](http://joebergeron.io/posts/post_two.html). Задача загрузчика - это найти на диске файл с *ядром* системы, загрузить его в память, и передать ему управление. Примеры файлов ядра: * `C:\msdos.sys` - для DOS; * `C:\Windows\System32\ntoskrnl.exe` - для Windows; * `/boot/vmlinuz` - символическая ссылка на zlib-сжатый образ ядра в Linux. Формат файла ядра - как правило, соответствует обычному исполняемому файлу (PE для Windows или ELF для Linux), но на него накладываются некоторые ограничения о размещении данных внутри файла, и кроме того, этот файл не может иметь зависимости от каких-либо библиотек. ### Загрузка ядра Linux, формат `multiboot`. Загрузчик GRUB загружает ELF-файл с ядром, распаковывает его при необходимости, и размещает по адресу, начиная с 1Мб. Далее загрузчик ищет Magic-метку заголовка `multiboot` в первых 32К загруженного файла ядра, сразу после которой идет набор флагов и контрольная сумма заголовка. После этого заголовка, в самом файле следует 16К памяти под стек, а сразу после него - начало программы, которую нужно выполнять. Таким образом, при компиляции ядер, необходимо строго указывать очерёдность различных секций, чтобы GRUB смог запустить ядро. Подробнее - [здесь](https://wiki.osdev.org/Bare_Bones). ### Запуск ядра Ядро регистрирует вектор прерываний, выполняет дальнейшую инициализацию оборудования, загружая при необходимости различные драйверы устройств. Когда ядро полностью загружено, то выполняется загрузка первой программы, которая выполняется в режиме пользователя: * `C:\command.com` - для DOS; * `C:\Windows\System32\smms.exe` - для Windows; * `/boot/initrd` - для Linux. ### Дальнейшие стадии запуска Процесс `initrd`, в зависимости от дистрибутива: * классический Unix-way: запускает набор shell-скриптов в одном из подкаталогов `/etc/init.d/rcX.d`, где `X` - уровень запуска по умолчанию, прописанный в файле `/etc/inittab`; * SystemD-way: запускает программу `systemd`, которая имеет свой набор конфигурационных файлов, по которым строит дерево зависимостей различных служб, и запускает их. ================================================ FILE: practice/asm/nostdlib_baremetal/toyos/Makefile ================================================ AS:=as --32 CC:=gcc -m32 CFLAGS:=-ffreestanding -O2 -Wall -Wextra -nostdlib CPPFLAGS:= LIBS:=-lgcc OBJS:=\ boot.o \ kernel.o \ all: myos.bin .PHONEY: all clean iso run-qemu myos.bin: $(OBJS) linker.ld $(CC) -T linker.ld -Wl,--build-id=none -o $@ $(CFLAGS) $(OBJS) $(LIBS) %.o: %.c $(CC) -c $< -o $@ -std=gnu99 $(CFLAGS) $(CPPFLAGS) %.o: %.s $(AS) $< -o $@ clean: rm -rf isodir rm -f myos.bin myos.iso $(OBJS) iso: myos.iso isodir isodir/boot isodir/boot/grub: mkdir -p $@ isodir/boot/myos.bin: myos.bin isodir/boot cp $< $@ isodir/boot/grub/grub.cfg: grub.cfg isodir/boot/grub cp $< $@ myos.iso: isodir/boot/myos.bin isodir/boot/grub/grub.cfg grub-mkrescue -o $@ isodir run-qemu: myos.iso qemu-system-i386 -cdrom myos.iso ================================================ FILE: practice/asm/nostdlib_baremetal/toyos/README.md ================================================ Example from [https://wiki.osdev.org/Bare_Bones](https://wiki.osdev.org/Bare_Bones) ================================================ FILE: practice/asm/nostdlib_baremetal/toyos/boot.s ================================================ /* Declare constants for the multiboot header. */ .set ALIGN, 1<<0 /* align loaded modules on page boundaries */ .set MEMINFO, 1<<1 /* provide memory map */ .set FLAGS, ALIGN | MEMINFO /* this is the Multiboot 'flag' field */ .set MAGIC, 0x1BADB002 /* 'magic number' lets bootloader find the header */ .set CHECKSUM, -(MAGIC + FLAGS) /* checksum of above, to prove we are multiboot */ /* Declare a multiboot header that marks the program as a kernel. These are magic values that are documented in the multiboot standard. The bootloader will search for this signature in the first 8 KiB of the kernel file, aligned at a 32-bit boundary. The signature is in its own section so the header can be forced to be within the first 8 KiB of the kernel file. */ .section .multiboot .align 4 .long MAGIC .long FLAGS .long CHECKSUM /* The multiboot standard does not define the value of the stack pointer register (esp) and it is up to the kernel to provide a stack. This allocates room for a small stack by creating a symbol at the bottom of it, then allocating 16384 bytes for it, and finally creating a symbol at the top. The stack grows downwards on x86. The stack is in its own section so it can be marked nobits, which means the kernel file is smaller because it does not contain an uninitialized stack. The stack on x86 must be 16-byte aligned according to the System V ABI standard and de-facto extensions. The compiler will assume the stack is properly aligned and failure to align the stack will result in undefined behavior. */ .section .bss .align 16 stack_bottom: .skip 16384 # 16 KiB stack_top: /* The linker script specifies _start as the entry point to the kernel and the bootloader will jump to this position once the kernel has been loaded. It doesn't make sense to return from this function as the bootloader is gone. */ .section .text .global _start .type _start, @function _start: /* The bootloader has loaded us into 32-bit protected mode on a x86 machine. Interrupts are disabled. Paging is disabled. The processor state is as defined in the multiboot standard. The kernel has full control of the CPU. The kernel can only make use of hardware features and any code it provides as part of itself. There's no printf function, unless the kernel provides its own header and a printf implementation. There are no security restrictions, no safeguards, no debugging mechanisms, only what the kernel provides itself. It has absolute and complete power over the machine. */ /* To set up a stack, we set the esp register to point to the top of the stack (as it grows downwards on x86 systems). This is necessarily done in assembly as languages such as C cannot function without a stack. */ mov $stack_top, %esp /* This is a good place to initialize crucial processor state before the high-level kernel is entered. It's best to minimize the early environment where crucial features are offline. Note that the processor is not fully initialized yet: Features such as floating point instructions and instruction set extensions are not initialized yet. The GDT should be loaded here. Paging should be enabled here. C++ features such as global constructors and exceptions will require runtime support to work as well. */ /* Enter the high-level kernel. The ABI requires the stack is 16-byte aligned at the time of the call instruction (which afterwards pushes the return pointer of size 4 bytes). The stack was originally 16-byte aligned above and we've since pushed a multiple of 16 bytes to the stack since (pushed 0 bytes so far) and the alignment is thus preserved and the call is well defined. */ call kernel_main /* If the system has nothing more to do, put the computer into an infinite loop. To do that: 1) Disable interrupts with cli (clear interrupt enable in eflags). They are already disabled by the bootloader, so this is not needed. Mind that you might later enable interrupts and return from kernel_main (which is sort of nonsensical to do). 2) Wait for the next interrupt to arrive with hlt (halt instruction). Since they are disabled, this will lock up the computer. 3) Jump to the hlt instruction if it ever wakes up due to a non-maskable interrupt occurring or due to system management mode. */ cli 1: hlt jmp 1b /* Set the size of the _start symbol to the current location '.' minus its start. This is useful when debugging or when you implement call tracing. */ .size _start, . - _start ================================================ FILE: practice/asm/nostdlib_baremetal/toyos/grub.cfg ================================================ menuentry "myos" { multiboot /boot/myos.bin } ================================================ FILE: practice/asm/nostdlib_baremetal/toyos/kernel.c ================================================ #include #include #include /* Hardware text mode color constants. */ enum vga_color { VGA_COLOR_BLACK = 0, VGA_COLOR_BLUE = 1, VGA_COLOR_GREEN = 2, VGA_COLOR_CYAN = 3, VGA_COLOR_RED = 4, VGA_COLOR_MAGENTA = 5, VGA_COLOR_BROWN = 6, VGA_COLOR_LIGHT_GREY = 7, VGA_COLOR_DARK_GREY = 8, VGA_COLOR_LIGHT_BLUE = 9, VGA_COLOR_LIGHT_GREEN = 10, VGA_COLOR_LIGHT_CYAN = 11, VGA_COLOR_LIGHT_RED = 12, VGA_COLOR_LIGHT_MAGENTA = 13, VGA_COLOR_LIGHT_BROWN = 14, VGA_COLOR_WHITE = 15, }; static inline uint8_t vga_entry_color(enum vga_color fg, enum vga_color bg) { return fg | bg << 4; } static inline uint16_t vga_entry(unsigned char uc, uint8_t color) { return (uint16_t) uc | (uint16_t) color << 8; } size_t strlen(const char* str) { size_t len = 0; while (str[len]) len++; return len; } static const size_t VGA_WIDTH = 80; static const size_t VGA_HEIGHT = 25; size_t terminal_row; size_t terminal_column; uint8_t terminal_color; uint16_t* terminal_buffer; void terminal_initialize(void) { terminal_row = 0; terminal_column = 0; terminal_color = vga_entry_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); terminal_buffer = (uint16_t*) 0xB8000; for (size_t y = 0; y < VGA_HEIGHT; y++) { for (size_t x = 0; x < VGA_WIDTH; x++) { const size_t index = y * VGA_WIDTH + x; terminal_buffer[index] = vga_entry(' ', terminal_color); } } } void terminal_setcolor(uint8_t color) { terminal_color = color; } void terminal_putentryat(char c, uint8_t color, size_t x, size_t y) { const size_t index = y * VGA_WIDTH + x; terminal_buffer[index] = vga_entry(c, color); } void terminal_putchar(char c) { terminal_putentryat(c, terminal_color, terminal_column, terminal_row); if (++terminal_column == VGA_WIDTH) { terminal_column = 0; if (++terminal_row == VGA_HEIGHT) terminal_row = 0; } } void terminal_write(const char* data, size_t size) { for (size_t i = 0; i < size; i++) terminal_putchar(data[i]); } void terminal_writestring(const char* data) { terminal_write(data, strlen(data)); } void kernel_main(void) { /* Initialize terminal interface */ terminal_initialize(); /* Newline support is left as an exercise. */ terminal_writestring("Hello, kernel World!\n"); } ================================================ FILE: practice/asm/nostdlib_baremetal/toyos/linker.ld ================================================ /* The bootloader will look at this image and start execution at the symbol designated as the entry point. */ ENTRY(_start) /* Tell where the various sections of the object files will be put in the final kernel image. */ SECTIONS { /* Begin putting sections at 1 MiB, a conventional place for kernels to be loaded at by the bootloader. */ . = 1M; /* First put the multiboot header, as it is required to be put very early early in the image or the bootloader won't recognize the file format. Next we'll put the .text section. */ .text BLOCK(4K) : ALIGN(4K) { *(.multiboot) *(.text) } /* Read-only data. */ .rodata BLOCK(4K) : ALIGN(4K) { *(.rodata) } /* Read-write data (initialized) */ .data BLOCK(4K) : ALIGN(4K) { *(.data) } /* Read-write data (uninitialized) and stack */ .bss BLOCK(4K) : ALIGN(4K) { *(COMMON) *(.bss) } /* The compiler may produce other sections, by default it will put them in a segment with the same name. Simply add stuff here as needed. */ } ================================================ FILE: practice/asm/x86_basics/README.md ================================================ # Ассемблер архитектуры x86 (32-bit, и немного про 64-bit) Основной reference по набору команд [преобразованный в HTML](https://www.felixcloutier.com/x86/). Reference по наборам команд MMX, SSE и AVX [на сайте Intel](https://software.intel.com/sites/landingpage/IntrinsicsGuide/). Неплохой учебник по ассемблеру x86 [на WikiBooks](https://en.wikibooks.org/wiki/X86_Assembly) ## 32-разрядный ассемблер в 64-битных системах Мы будем использовать 32-разрядный набор инструкций. На 64-битных архитектурах для этого используется опция компилятора gcc `-m32`. Кроме того, необходимо установить стек 32-разрядных библиотек. В Ubuntu это делается всего одной командой: ``` sudo apt-get install gcc-multilib ``` ## Синтаксис AT&T и Intel Исторически сложилось два синтаксиса языка ассемблера x86: синтаксис AT&T, используемый в UNIX-системах, и синтаксис Intel, используемый в DOS/Windows. Различие, в первую очередь, относится к порядку аргументов команд. Компилятор gcc по умолчанию использует синтаксис AT&T, но с указанием опции `-masm=intel` может переключаться в синтаксис Intel. Кроме того, можно указать используемый синтаксис первой строкой в тексте самой программы: ```nasm .intel_syntax noprefix ``` Здесь параметр `noprefix` после `.intel_syntax` указывает на то, что помимо порядка аргументов, соответствующих синтаксису Intel, ещё и имена регистров не должны начинаться с символа `%`, а константы - с символа `$`, как это принято в синтаксисе AT&T. Мы будем использовать именно этот синтаксис, поскольку с его использованием написано большинство доступной документации и примеров, включая документацию от производителей процессоров. ## Регистры процессора общего назначения Исторически семество процессоров x86 унаследовало набор 8-битных регистров общего назначения семества 8080/8085, которые назывались `a`, `b`, `c` и `d`. Но поскольку процессор 8086 стал 16-битным, то регистры стали назваться `ax`, `bx`, `cx` и `dx`. В 32-битных процессорах они называются `eax`, `ebx`, `ecx` и `edx`, в 64-битных `rax`, `rbx`, `rcx` и `rdx`. Кроме того, в x86 есть регистры "двойного назначения", которые можно использовать, в том числе, в качестве регистров общего назначения, если пользоваться ограниченным подмножеством команд процессора: * `ebp` - верхняя граница стека; * `esi` - индекс элемента массива, из которого выполняется копирование; * `edi` - индекс элемента массива, в который выполняется копирование. Регистр `esp` содержит указатель на нижнюю границу стека, поэтому произвольным образом его использовать не рекомендуется. ### Регистры x86-64 64-разрядные регистры для архитектуры x86-64 именуются начиная с буквы `r`. Помимо регистров `rax`...`rsi`, `rdi` можно использовать регистры общего назначение `r9`...`r15`. Указатель стека хранится в `rsp`, верхняя граница стекового фрейма - в `rbp`. Младшие 32-разрядные части регистров `rax`...`rsi`,`rdi`,`rsp`,`rbp` можно адресовать по именам `eax`...`esi`,`edi`,`esp`,`ebp`. При записи значений по 32-битным именам регистров, старшие 32 разряда обнуляются, что приемлемо для операций над 32-разрядными беззнаковыми значениями. Для работы со знаковыми 32-разрядными значениями, например типом `int`, необходимо предварительно выполнять операции *знакового расширения* с помощью команды `movslq` ## Некоторые инструкции **Для синтаксиса Intel** первым аргументов команды является тот, значение которого будет модифицировано, а вторым - которое остается неизменным. ```nasm add DST, SRC /* DST += SRC */ sub DST, SRC /* DST -= SRC */ inc DST /* ++DST */ dec DST /* --DST */ neg DST /* DST = -DST */ mov DST, SRC /* DST = SRC */ imul SRC /* (eax,edx) = eax * SRC - знаковое */ mul SRC /* (eax,edx) = eax * SRC - беззнаковое */ and DST, SRC /* DST &= SRC */ or DST, SRC /* DST |= SRC */ xor DST, SRC /* DST ^= SRC */ not DST /* DST = ~DST */ cmp DST, SRC /* DST - SRC, результат не сохраняется, */ test DST, SRC /* DST & SRC, результат не сохраняется */ adc DST, SRC /* DST += SRC + CF */ sbb DST, SRC /* DST -= SRC - CF */ ``` **Для синтаксиса AT&T** порядок аргументов - противоположный, то есть команда `add %eax, %ebx` вычислит сумму `%eax` и `%ebx`, после чего сохранит результат в регистр `%ebx`, который указан вторым аргументом. ## Флаги процессора В отличии от процессоров ARM, где обновление регистра флагов производится только при наличии специального флага в команде, обозначаемого суффиксом `s`, в процессорах Intel флаги обновляются всегда большинстом инструкций. Флаг `ZF` устанавливается, если в результате операции был получен нуль. Флаг `SF` устанавливается, если в результате операции было получено отрицательное число. Флаг `CF` устанавливается, если в результате выполнения операции произошел перенос из старшего бита результата. Например, для сложения `CF` устанавливается если результат сложения двух беззнаковых чисел не может быть представлен 32-битным беззнаковым числом. Флаг `OF` устанавливается, если в результате выполняния операции произошло переполнение знакового результата. Например, при сложении `OF` устанавливается, если результат сложения двух знаковых чисел не может быть представлен 32-битным знаковым числом. Обратите внимание, что и сложение `add`, и вычитание `sub` устанавливают одновременно и флаг `CF`, и флаг `OF`. Сложение и вычитание знаковых и беззнаковых чисел выполняется совершенно одинаково, и поэтому используется одна инструкция и для знаковой, и для беззнаковой операции. Инструкции `test` и `cmp` не сохраняют результат, а только меняют флаги. ## Управление ходом программы Безусловный переход выполняется с помощью инструкции `jmp` ```nasm jmp label ``` Условные переходы проверяют комбинации арифметических флагов: ```nasm jz label /* переход, если равно (нуль), ZF == 1 */ jnz label /* переход, если не равно (не нуль), ZF == 0 */ jc label /* переход, если CF == 1 */ jnc label /* переход, если CF == 0 */ jo label /* переход, если OF == 1 */ jno label /* переход, если OF == 0 */ jg label /* переход, если больше для знаковых чисел */ jge label /* переход, если >= для знаковых чисел */ jl label /* переход, если < для знаковых чисел */ jle label /* переход, если <= для знаковых чисел */ ja label /* переход, если > для беззнаковых чисел */ jae label /* переход, если >= (беззнаковый) */ jb label /* переход, если < (беззнаковый) */ jbe label /* переход, если <= (беззнаковый) */ ``` Вызов функции и возврат из неё осуществляются командами `call` и `ret` ```nasm call label /* складывает в стек адрес возврата, и переход на label */ ret /* вытаскивает из стека адрес возврата и переходит к нему */ ``` Кроме того, есть составная команда для организации циклов, которая подразумевает, что в регистре `ecx` находится счётчик цикла: ```nasm loop label /* уменьшает значение ecx на 1; если ecx==0, то переход на следующую инструкцию, в противном случае переход на label */ ``` ## Адресация памяти В отличии от RISC-процессоров, x86 позволяет использовать в качестве **один из аргументов** команды как адрес в памяти. **В синтаксисе AT&T** такая адресация записывается в виде: `OFFSET(BASE, INDEX, SCALE)`, где `OFFSET` - это константа, `BASE` и `INDEX` - регистры, а `SCALE` - одно из значений: `1`, `2`, `4` или `8`. Адрес в памяти вычисляется как `OFFSET+BASE+INDEX*SCALE`. Параметры `OFFSET`, `INDEX` и `SCALE` являются опциональными. При их отсутсвтвии подразумевается, что `OFFSET=0`, `INDEX=0`, `SCALE` равен размеру машинного слова. **В синтаксисе Intel** используется более очевидная нотация: `[BASE + INDEX * SCALE + OFFSET]`. ## Соглашения о вызовах для 32-разрядной архитектуры Возвращаемое значение 32-разрядного типа функции записывается в регистр `eax`, для возврата 64-разрядного значения используется пара `eax` и `edx`. Вызываемая функция обязана сохранять на стеке значения регистров общего назначения `ebx`, `ebp`, `esi` и `edi`. Аргументы могут передаваться в функцию различными способами, в зависимости от соглашений, принятых в ABI. ### Соглашения cdecl и stdcall Соглашения о передаче аргументов, используемые на 32-разрядных системах архитектуры x86. Все аргументы функций складываются справа-налево в стек, затем вызывается функция, которая адресует аргументы через указатель `ebp` или `esp` с некоторым положительным смещением. Пример: ```c char * s = "Name"; int value1 = 123; double value2 = 3.14159; printf("Hello, %s! Val1 = %d, val2 = %g\n", s, value1, value2); ``` Здесь перед вызовом `printf` в стек будут сложены значения, переменных, прежде чем вызвана функция: ```nasm push value2 push value1 push s push .FormatString call printf ``` В случае использования соглашения `stdcall`, **вызываемая** функция обязана удалить из стека переданные её аргументы после их использования. В случае использования соглашения `cdecl`, **вызывающая** функция обязана удалить из стека те переменные, которые были переданы в вызываемую функцию. На языках Си/С++ используемые соглашения можно указывать в специцикаторах функций, например: ``` void __cdecl regular_function(int arg1, int arg2); #define WINAPI __stdcall void WINAPI winapi_function(int arg1, int arg2); ``` Соглашение `stdcall` сейчас используется в основном в операционной системе Windows для обращения к функциям WinAPI. Во всех остальных случаях на 32-разрядных системах используется `cdecl`. ### Соглашение fastcall Если требуется передать в функцию немного целочисленных аргументов, то можно использовать регистры, как в архитектуре ARM. Такое соглашение называется `fastcall`. Соглашение `fastcall` используется для вызова функций ядра (системных вызовов) в UNIX-подобных системах. В частности, в Linux регистр `eax` используется для передачи номера системного вызова, а регистры `ebx`, `ecx` и `edx` - для передачи целочисленных аргументов. Аналогичный подход используется и в архитектуре x86-64, где доступных регистров больше, чем в 32-разрядной архитектуре x86. ## Соглашения о вызовах для 64-разрядной архитектуры SystemV AMD64 ABI Целочисленные аргументы передаются последовательно в регистрах: `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`. Если передается более 6 аргументов, то оставшиеся - через стек. Вещественные аргументы передаются через регистры `xmm0`...`xmm7`. Возвращаемое значение целочисленного типа должно быть сохранено в `rax`, вещественного - в `xmm0`. Вызываемая функция обязана сохранять на стеке значения регистров общего назначения `rbx`, `rbp`, и регистры `r12`...`r15`. Кроме того, при вызове функции для 64-разрядной архитектуры есть дополнительное требование - перед вызовом функции стек должен быть выровнен по границе 16 байт, то есть необходимо уменьшить значение `rsp` таким образом, оно было кратно 16. Если кроме регистров задействуется стек для передачи параметров, то они должны быть прижаты к нижней выровненной границе стека. Для функций гарантируется 128-байтная "красная зона" в стеке ниже регистра `rsp` - область, которая не будет затронута внешним событием, например, обработчиком сигнала. Таким образом, можно задействовать для адресации локальных переменных память до `rsp-128`. ================================================ FILE: practice/asm/x86_fpmath/README.md ================================================ # Вещественная арифметика на x86 Основной reference по набору команд [преобразованный в HTML](https://www.felixcloutier.com/x86/). Reference по наборам команд MMX, SSE и AVX [на сайте Intel](https://software.intel.com/sites/landingpage/IntrinsicsGuide/). ## Сопроцессор x87 Операции над вещественными числами выполняются отдельными блоками процессора. Исторически сложилось, что для вещественной арифметики использовался отдельный *сопроцессор*, а начиная с процессоров 486 (1991 год) этот сопроцессор был интегрирован в кристалл основного процессора. Таким образом, в целях совместимости со старым кодом, выполнение вещественных операций над числами с плавающей точкой выполняется из предположения наличия сопроцессора. Компилятор `gcc` использует по умолчанию именно этот способ работы с вещественными числами для 32-разрядной архитектуры (эквивалентно опции `-mfpmath=387`). Для 64-разрядной архитектуры по умолчанию используется набор команд SSE (`-mfpmath=sse`). Взаимодействие с сопроцессором x87 организовано в форме записи операндов в стек и выполнения операций над элементами этого стека. Команды сопроцессора обычно начинаются с буквы `f`, и оперируют с регистрами, которые обозначаются от `st(0)` (вершина стека) до `st(7)` (последний регистр FPU). Выполнять арифметические FPU-операции можно над любыми регистрами этого стека, но операции, позволяющие взаимодействие c памятью, возможны только через вершину стека `st(0)`. Основные инструкции x86: ``` fld SIZE ptr ADDR // загрузить в стек значение из памяти fld REG // поместить на вершину стека значение из другого регистра st(X) fld1 // поместить на вершину стека значение 1 fldz // 0 fldpi // π fst SIZE ptr ADDR // сохранить из стека в память fild SIZE ptr ADDR // загрузить целое число в стек fist SIZE ptr ADDR // сохранить целое число из стека fcom // сравнение st(0) с памятью fcomi // сравнение st(0) с st(i) fadd fsub fmul fdiv // операции над вещественными операндами fiadd fisub fimul fidiv // операции над целочисленными операндами ``` ## Регистры MMX/SSE/AVX/AVX-512 В современных Intel/AMD процессорах есть 16 регистров (в 32-разрядном режиме доступны только 8), которые предназначены как для вещественных операций, так и для целочисленных. 128-битные регистры MMX/SSE именуются `xmm0`...`xmm7`, `xmm8`...`xmm15`. 256-битные регистры AVX `ymm0`...`ymm15` подразумевают, что их младшие 128 бит совпадают с регистрами MMX/SSE. 512-битные регистры AVX-512 (новые Xeon и Core i9) `zmm0`...`zmm15` подразумевают, что младшие 256 бит совпадают с регистрами AVX. ## Скалярные инструкции SSE Несмотря на свой большой размер, регистры SSE можно использовать как обычные скалярные, что намного эффективнее, чем x87 FPU. В отличии от регистров в стеке `st(0)`...`st(7)`, все регистры SSE являются равнозначными. ``` // Копирование регистр-регистр и регистр-память movsd DST, SRC // пересылка double movss DST, SRC // пересылка float // Арифметические addsd DST, SRC // DST += SRC, double addss DST, SRC // DST += SRC, float subsd DST, SRC // DST -= SRC, double subss DST, SRC // DST -= SRC, float mulsd DST, SRC // DST *= SRC, double mulss DST, SRC // DST *= SRC, float divsd DST, SRC // DST /= SRC, double divss DST, SRC // DST /= SRC, float sqrtsd DST, SRC // DST = sqrt(SRC), double sqrtss DST, SRC // DST = sqrt(SRC), float maxsd DST, SRC // DST = max(DST, SRC), double maxss DST, SRC // DST = max(DST, SRC), float minsd DST, SRC // DST = min(DST, SRC), double minss DST, SRC // DST = min(DST, SRC), float // Преобразования cvtsd2si DST, SRC // double -> int cvtsi2sd DST, SRC // int -> double // Сравнения (операция DST-SRC, которая меняет флаги) comisd DST, SRC // для double comiss DST, SRC // для float ``` ## Соглашения о вызовах для x86 (32 бит) Все вещественные аргументы передаются в функцию через стек. Возвращаемое вещественное значение должно быть сохранено в регистре `st(0)`, причем это требование необходимо соблюдать **даже при использовании регистров SSE**. Это необходимо для того, чтобы вызывающая функция могла использовать вещественный результат, независимо от способа реализации вызываемой функции. Переместить результат из регистра SSE в регистр x87 можно только с ипользованием памяти: ``` sub esp, 8 // выделяем 8 байт на стеке movsd [esp], xmm0 // копируем из xmm0 в память fld qword ptr [esp] // загружаем из памяти в стек x87 add esp, 8 // освобождаем память на стеке ``` ## Векторные инструкции SSE и intrisics-функции на Си Между регистрами можно выполнять *векторные* операции, то есть операции сразу над несколькими 8, 16, 32 или 64-битными значениями, которые хранятся в паре 128-битных регистров. Общий вид таких команд следующий: ``` OPERATION p [s|d] ``` где `OPERATION` - это одна из операций `add`, `mul` и т.д., буква `p` в названии команды является сокращением от `p`acked, а `s` или `d` - это `s`ingle или `d`ouble точность вещественных чисел. Загрузка/сохранение выполняется вариантами команды `mov`: ``` mov[ap|up][s|d] DST, SRC ``` где `ap` - загрузка/сохнанение из памяти, выровненной по границе размера регистра (16 байт), `up` - для невыровненной памяти. Использование операндов в памяти для операций, отличных от `mov`, возможно только для выровненной памяти. Для задействования векторных инструкций не обязательно использовать язык ассемблера. Компиляторы Intel и `gcc` имеют поддержку псевдо-функций, объявленных в заголовочных файлах вида `*intrin.h`, которые транслируются в эти инструкции при компиляции. Подробный Reference [доступен здесь](https://software.intel.com/sites/landingpage/IntrinsicsGuide/). ================================================ FILE: practice/bash-grep-sed/README.md ================================================ # Программирование командной строки ## Командные интерпретаторы Командная строка в UNIX-подобных системах выполняет текстовые команды, и этот текст может быть записан в виде построчной программы, пригодной к выполнению из файла. Скрипт командной строки обычно начинается со строчки вида: ``` #!/usr/bin/env bash ``` Данная строка синтаксически является комментарием во многих языках программирования, но имеет специальное назначение в UNIX-подобных системах. Исполняемые файлы, которые начинаются с символов `#!` подразумевают запуск программы-интерпретатора, указанной после `#!`, которой передается в качестве аргумента передается имя файла скрипта. В программы-интерпретатора должен быть указан ее полный путь. Расположение некоторых интерпретаторов, например `/bin/sh`, является стандартизированным для всех UNIX-подобных систем, для других, например bash, этот путь может быть как `/bin/bash`, так и `/usr/bin/bash`  или `/usr/local/bin/bash`, - гарантировать путь однозначно нельзя. Для поиска интерпретатора в переменной окружения `PATH` используется утилита `/usr/bin/env`, которая устанавливает переменные окружения, включая `PATH`, и запускает программу, переданную ей в качестве аргумента. ## Язык программирования SH В разных Linux-системах и других UNIX-подобных системах используются различные интерпретаторы командной строки, общим предком которых является классический интерпретатор `/bin/sh`. При этом, сам интерпретатор `/bin/sh` является символической ссылкой на используемый в дистрибутиве интерпретатор по умолчанию (за исключением MacOS, где `/bin/sh` - это `bash` старой версии, в то время как в системе используется `zsh`). Можно считать, что программа, ориентированная на `/bin/sh` может быть запущена на любой UNIX-подобной системе, но при написании таких скриптов необходимо ориентироваться на общее подмножество функциональности различных интерпретаторов, которое регламентировано стандартом POSIX. В дальнейшем мы будем использовать интерпретатор `bash`, который присутствует во всех популярных дистрибутивах Linux, и обладает широкой фунциональностью. Каждая команда shell-скрипта может располагаться на отдельной строке, либо заканчиваться символом точки с запятой, если необходимо записать в одну строку несколько команд. Команды скриптов, в большинстве случаев, - это внешние программы, которые располагаются в одном из каталогов, перечисленных в переменной окружения `PATH`, но некоторые команды не могут быть реализованы как отдельные программы, поскольку изменяют текущее окружение, что не может быть сделано внешней программмой. Примерами таких команд являются: * `cd` - изменение текущего каталога; * `export` - делают переменную доступной дочерним процессам; * `read` - читает текст из файла или стандартного потока ввода, и записывает результат в переменную; * `ulimit` - устанавливает ограничения ресурсов на текущий сеанс; * `exit` - завершает работу командного интерпретатора. Кроме того, поскольку запуск внешних программ является ресурсозатратной операцией, в некоторых оболочках отдельные часто используемые команды реализованы как встроенные, хотя их функциональность дублируется внешними одноименными программами, например команда `echo` для интерпретаторов `bash` и `zsh`, или команда `[` для оболочки `bash` . Полный список встроенных команд для текущей оболочки можно получить командой `man builtins`. Команды могут иметь аргументы, которые разделяютя пробелами. В случае, если аргумент должен содержать пробел, или какой-либо другой символ, имеющий специальное назначение, такой аргумент нужно заключать в кавычки. Также зарезервированные символы можно экранировать с помощью символа `\`. Список зарезервированных символов, помимо пробельных, которые нужно экранировать, или заключать в кавычки: ``` | & ; < > ( ) $ ` \ " ' * ? [ # ~ = % ``` Экранирование символа переноса строки выполняется специальным образом, что обусловлено необходимостью читабельности кода скрипта: `$'\n'`. ### Особенности синтаксиса и специальные символы В скриптах используются три вида кавычек, которые имеют различное семантическое назначение: * 'одинарные кавычки' - сохраняет текст без изменений; * \`обратные одинарные кавычки\` - выполняют команду и результатом является вывод этой команды; * "двойные кавычки" - внутри них возможно экранирование символом \, подстановка значений переменных (начинаются с символа $), и возможно выполнение команд с помощью вложенных `обратных кавычек`. Переменные объявляются символом `=`, причем пробелы вокруг этого символа не допускаются. Использовать переменные можно в виде `$переменная` или `${переменная}`. ``` # значение 123 var1=123 # пустое значение var2= # строка var3="hello world" # так нельзя - будет ошибка var4 = 123 # обращение к переменным # вывод будет: $var1 hello world echo '$var1' "$var2" "$var3" "$var_not_exist" ``` Использование несуществующей переменной не приводит к ошибке, - будет просто пустое значение. ### Выполнение команд и получение их результата Если необходимо сохранить вывод команды в переменную, то используется один из двух способов: * \`обратные одинарные кавычки\`, между которыми заключена команда и ее аргументы; * заключение к конструкцию `$()`. Если вывод команды содержит в конце символы перевода строк, то они удаляются. Пример: ``` os_name=`uname -s` arch_name=$(uname -m) echo "OS is $os_name running on $arch_name" # OS is Linux running on x86_64 ``` Если необходимо получить код возврата команды, а не результат ее вывода, то можно использовать переменную `$?` сразу после ее выполнения. ### Специальные переменные и аргументы * `$?` - целочисленный код возврата последней команды; * `$0`...`$9` - аргументы команды от 0 до 9, при этом `$0` - имя самого скрипта; * `$#` - целочисленное значение количества аргументов; * `$@` - список всех аргументов, начиная с первого; * `$*` - строка, которая содержит список всех аргументов, начиная с первого. ### Объявление и вызов функций Функции - это команды, которые доступны только из текущего скрипта, которым можно передавать аргументы, и они могут возвращать текст с помощью записи на "стандартный поток вывода". Интерпретаторы `bash` и `zsh` поддерживают три вида синтаксиса объявлений: ``` # 1. Только имя и скобки very_important_function() { # реализация функции } # 2. Полный синтаксис function very_important_function() { # реализация функции } # 3. Без скобок function very_important_function { # реализация функции } # вызов функции с двумя аргументами, и сохранением результата value=$(very_important_function hello world) # вывзов функции без сохранения возвращаемого результата very_important_function hello world ``` Если необходимо обеспечить совместимость с произвольным интерпретатором POSIX `sh`, то можно использовать только первый вариант объявления. Аргументы в функцию передаются точно так же, как и в команду, и доступны через специальные переменные. Все переменные, объявляенные внутри функции, становятся доступными глобально после ее завершения. Если переменные нужно только локально, то в `bash` и `zsh` перед объявлением переменной можно использовать ключевое слово `local`. Функции можно импортировать из другого файла, используя синтаксис `. имя_файла`. ### Перенаправление вывода Для передачи данных от одной функции/команды к другой, не обязательно сохранять результаты в переменную, можно передавать из через механизм перенаправления, используя оператор `|`. ``` function f() { # вывод kek и списка аргументов echo "kek $*" } function g() { # замена e на E sed 's/e/E/g' } function h() { # замена d на первый аргумент функции echo $0 sed "s/d/$1/g" } f first second third | g | h Meaow # kEk first sEconMeaow thirMeaow # команда wc -c подсчитвает количество байт f | wc -c # 5 ``` ### Условное выполнение последовательности команд Результатом работы команды, помимо вывода, является целочисленный код возврата, причем целое число должно быть в диапазоне от 0 до 127. Значение 0 означает успешное завершение команды, остальные значения, - признак "ошибки" или ложного значения. Код завершения предыдущей выполненной команды хранится в переменной `$?`. При объявлении функций код возврата определяется кодом возврата последней выполненной внутри функции команды, либо задается с помощью оператора `return`. Команды можно объединять в последовательности, которые выполняются в зависимости от результата выполнения предыдущей команды: * `cmd1 && cmd2` - `cmd2` будет выполнена, если `cmd1` выполнена успешно, а итоговый результат - это код возврата `cmd2`; * `cmd1 || cmd2` - `cmd2` будет выполнена, если не удалось успешно выполнить `cmd1`, результат - либо значение 0, либо код возврата `cmd2`. Выполнение цепочки команд можно заключать в круглые скобки для указания приоритетов логических операций. ``` function f() { echo "I'm function f" return 0 } function g() { echo "I'm function g" return 5 } function h() { echo "I'm function h" return 0 } f && g && h # вывод только от f и g, но не h echo "---" f && (g || h) # вывод от f, g и h ``` Предусмотрены две программы, которые не делают абсолютно ничего, а только возвращают код 0 или 1: это команда `true`, и команда `false`. Они предназначены для использования внутри таких "логических выражений", например, если нужно подавить код ошибки для необязательной операции: ``` rm -f файл_который_не_существует || true # всегда будет успешный код возврата ``` ### Конструкция условия и команда [ Логические условия могут быть использованы условных конструкций, как в обычных языках программирования. ``` if true then # эта часть всегда будет выполняться fi if false then # это не будет выполняться никогда fi ``` Аргументом команды `if` может быть любая команда. Истинным условием считается нулевой код возврата, а ложным - ненулевой. Для выполнения различных логических операций служит конструкция `[ .... ]`, которая реализована, в общем случае, с помощью отдельной команды `[`. * `$x -eq $y` - истина, если значения `$x` и `$y` равны; * `$x -nq $y` - истина, если значения `$x` и `$y` не равны; * `$x -gt $y` - истина, если значение `$x` > `$y`; * `$x -lt $y` - истина, если значение `$x` < `$y`; * `$x -ge $y` - истина, если значение `$x` >= `$y`; * `$x -le $y` - истина, если значение `$x` <= `$y`; * `-n $str` - истина, если строка `$str` не пустая; * `-z $str` - истина, если строка `$str` пустая; * `$str1 = $str2` - истина, если строки `$str1` и `$str2` равны; * `-e $pathname` - истина, если существует путь `$pathname`; * `-f $filename` - истина, если существует обычный файл `$filename`; * `-d $dirname` - истина, если существует каталог `$dirname`; * `-x $filename` - истина, если существует обычный файл `$filename`, и он является выполняемым. Внутри конструкции `[ ... ]` можно использовать круглые скобки для указания приоритетов, и оператор отрицания `!`. Важно особенностью этой конструкции является то, что между символами `[`, `]` и разными операторами внутри конструкции обязавтельно должны быть пробельные символы, поскольку это вызов команды с аргументами. ### Циклы Простейшим циклом является цикл `while`, аргумент которого точно такой же, как у конструкции `if`. ``` while true do # не только простейшая, но и самая # опасная конструкция, поскольку цикл # может никогда не завершиться done ``` Конструкция `for` предназначена для итерации по элементам списка. ``` # 1. Итерация по элементам простого списка for item in i love akos do echo "$item" done # 2. Итерация по элемента генерируемого по маске списка файлов for filename in *.txt do echo "$filename might be plain text" done ``` Интерпретаторы `bash` и `zsh` имеют еще одну, нестандартную для POSIX `sh`, конструкцию циклов, которая синтаксически близка к Си-подобным языкам. ``` # только bash/zsh for (( i=0; i<10; i++ )) do echo "$i" done ``` ### Internal Field Separator Элементы списка разделяются символом пробел в самом скрипте, если они перечислены после ключевого слова `in`, но могут также быть прочитаны из файла, либо получены из произвольной строки. ``` for item in $(echo "i love akos") do echo "$item" done # i # love # akos ``` В этом случае разделителями считаются подряд идущие последовательности пробельных символов: пробел, табуляция и символ перевода строки. Часто бывает необходимо переопределить символ разделителя, например для обработки текстовых файлов определенного формата. Для этого предназначена специальная переменная интерпретатора `IFS` (аббривеатура от Internal Field Separator). ``` IFS=💞 for item in $(echo "i💞love💞💞💞akos") do echo "$item" done # i # love # # # akos ``` После переопределения символа в переменной `IFS` , разделители не группируются. В качестве разделителя можно использовать только односимвольную строку, причем интерпретаторы `bash` и `zsh` корректно обрабатывают многобайтные символы Юникода, но это не гарантируется для других интерпретаторов. Чтение из файла можно организовать либо через команду `cat`, либо используя встроенную функцию `read` (обычно используется внутри цикла `while`), которая читает очередную лексему, ограниченную разделителем из `IFS`, и возвращает код 0, в случае успешного чтения. ### Арифметика Командный интерпретатор `sh` позволяет вычислять произвольные арифметические выражения, но с принципиальным ограничением: допускается только знаковая целочисленная арифметика. Синтаксически операции эквивалентны таковым в других языках программирования, а сами выражения заключаются в конструкцию `$(( ... ))`. В отличии от команды `[`, круглые скобки не являются внешней командой, поэтому пробельные символы не обязательны. ``` a=5 b=3 c=$(($a+$b)) d=$(($a/$b)) echo "a = $a, b = $b, c = $c, d = $d" # a = 5, b = 3, c = 8, d = 1 ``` Для вычислений с вещественнозначными значениями можно использовать простой консольный калькулятор `bc` (Basic Calculator). Эта программа выполняет вычисление арифметических выражений, позволяет использовать функции логарифма, экспоненты и тригонометрические функции. ``` echo '(1+3)*2' | bc # 8 # по умолчанию используется целочисленная арифметика, # флаг -l подключает дополнительную функциональность echo '(1+3)/2.5' | bc -l # 1.60000000000000000000 ``` ### Массивы Массивы являются нестандартным расширением `sh`, реализованным (по-разному) в командных интерпретаторах `bash` и `zsh`. Переменные массива объявляются как список, разделенный пробельными символами, заключенный в круглые скобки. Пустой массив обявляется как `()`. Элементы массива можно индексировать целыми числами с 0 (для `bash`) или с 1 (для `zsh`). Индексы указываются в квадратных скобках, поэтому для исключения неоднозначности операторов, при использовании значения из массива, обязательно заключать переменную с индексом в фигурные скобки. Адресация массива целиком (например, для вывода), а не его отдельного элемента осуществляется с указанием индекса `[@]`. Размер массива определяется как `${#массив[@]}`. ``` # пример для bash - индексация с 0 array=(1 2 3 4 5 6) array_size=${#array[@]} for (( i=0; i<$array_size; i++ )) do # удвоенное значение array[$i]=$(( ${array[$i]} * 2 )) done echo "${array[@]}" ``` ## Обработка текстов в командной строке Задачи обработки текстов возникают очень часто, и во многих случаях для их решения совершенно не обязательно писать программы на высокоуровневых языках программирования, - можно воспользоваться стандартными утилитами среды POSIX. ### Регулярные выражения Регулярное выражение - это текстовый шаблон, включающий в себя специальные символы-подставновки, который предназначен для поиска и замен в тексте. Синтаксис описания регулярных выражений бывает различный, наиболее распространенный из них - это в формате языка программирования Perl, который также используется во многих других языках программирования. Стандарт POSIX для регулярных выражений при этом является менее функциональным, и определяет два уровня языка описания: базовый (BRE) и расширенный (ERE). Расширенный синтаксис POSIX отличается от базового тем, что не требует обязательного экранирования символов скобок, а также вводит операции `?`, `+` и `|`. В дальнейшем будем использовать именно расширенный синтаксис (утилиты `sed` и `grep` требуют явного указания ключа `-E` для работы в расширенном синтаксисе). Для тестирования регулярных выражений можно использовать веб-приложение [regex101.com](https://regex101.com), которое не поддерживает синтаксис POSIX, поэтому при написании выражений нужно не забывать о том, что не поддерживаются PCRE-специфичные конструкции, например определения классов символов через символ `\`. Символы-подстановки, используемые в регулярных выражениях: * `^` - начало строки; * `.` - любой символ; * `[ ]` - любой символ или диапазон символов, из перечисленных в квадратных скобках; * `[^ ]` - то же самое, но с отрицанием; * `$` - признак конца строки; * `( )` - группа символов или подстановок; * `*` - повторение предыдущего символа 0 или более раз; * `?` - повторение предыдущего символа 0 или 1 раз (только ERE); * `+` - повторение предыдущего символа 1 или более раз (только ERE); * `{n}` - повторение предыдущего символа ровно `n` раз (только ERE); * `{m, n}` - повторение предыдущего символа от `m` до `n` раз (только ERE); * `|` - выбор одного из вариантов, между которыми встретился этот символ (только ERE). ### Утилита grep Утилита `grep` построчно просматривает текст из файла или стандартного потока ввода, и выполняет фильтрацию содержимого, оставляя только те строки текста, которые соответствуют шаблону. Пример. Содержимое исходного файла `test.txt`: ``` мама мыла раму папа кушал сидр акос любят все мы все умрем физтех чемпион физтех лучше всех ``` ``` # только одна строка, которая содержит слово "мама" > grep мама test.txt мама мыла раму # строки, которые содержат слова "мама" и "папа" > grep .а.а test.txt мама мыла раму папа кушал сидр # все строки, которые начинаются со слова из четырех букв > grep -E '^.{4} ' test.txt мама мыла раму папа кушал сидр акос любят все ``` В последнем примере обратите внимание на следующие особенности: * необходима опция `-E`, поскольку используется конструкция `{n}`, определенная в расширенном стандарте; * регулярное выражение содержит символ пробела, поэтому заключено в кавычки. Части регулярного выражения, которые заключены в круглые скобки, запоминают вхождение текста, и могут быть использованы в самом шаблоне. Эти вхождения нумеруются от `\1` до `\9`. ``` # найти все строки, в которых слово из алфавита [а-яА-Я] # повторяется через одно слово > grep -E '([а-яА-Я]+) .+ \1' физтех чемпион физтех лучше всех ``` Вместе с утилитой `grep` часто используется утилита `cut`, которая в найденной строке выбирает определенный "столбец", считая разделителем либо символ табуляции, либо какой-то произвольно заданный символ. ``` # найдем все вторые слова в строках, которые начинаются # со слова из четырех букв > grep -E '^.{4} ' test.txt | cut -d ' ' -f 2 мыла кушал любят ``` ### Потоковый редактор sed Помимо поиска, второй важный класс задач со строками, - это редактирование текста. Для автоматизации используются командные текстовые редакторы, такие как `sed` или `awk`. В отличии от обычных текстовых редакторов с пользовательским интерфейсом, потоковые редакторы оперируют набором команд редактирования. Команды `sed` разделяются символом `;` и выполняют одно из действий: вставка в начало, вставка в конец, удаление и замена текста. Общий вид команд: `[ПОЗИЦИЯ]ДЕЙСТВИЕ`, где `ПОЗИЦИЯ` - это необязательная часть команды, определяющая позицию курсора редактирвоания, `ДЕЙСТВИЕ` - однобуквенная команда с возможными аргументами. Основные команды редактирования: * `d` удаление; * `a` добавление текста после курсора; * `i` добавление текста перед курсором; * `s` замена текста по шаблону. `ПОЗИЦИЯ` описывается одним в одном из форматов: * `ЧИСЛО` - номер строки, которые нумеруются с 1; * `ЧИСЛО~ШАГ` - номер строки с повторением действия через определенное количество шагов; * `$` - последняя строка; * `/РЕГУЛЯРКА/` - все строки, сопоставленные с шаблоном. Набор команд является обязательным позиционным аргументом для команды `sed`. Как и для утилиты `grep`, если предполагается использование расширенного синтаксиса регулярных выражений, необходим флаг `-E`. Если утилите `sed` не указывать имя входного файла, то подразумевается взаимодействие со стандартными потоками ввода и вывода. Если указываются файлы (их может быть несколько), то прозводится чтение из указанных файлов, причем по умолчанию используется сквозная нумерация строк по всем файлам. Для того, чтобы каждый файл обрабатывался по-отдельности, необходима опция `-s`. Опция `-i`, также как и для `clang-format`, подразумевает сохранение изменений в исходный файл, а не вывод результата на стандартный поток вывода. Используйте эту опцию с осторожностью. #### Примеры ``` # удалить первую и последнюю строки из файла > sed '1d; $d' test.txt папа кушал сидр акос любят все мы все умрем ``` ``` # удалить все нечетные строки из файла > sed '1~2d' test.txt папа кушал сидр мы все умрем ``` ``` # вставить строку #!/bin/cat в начало файла > sed '1i#!/bin/cat' test.txt #!/bin/cat мама мыла раму папа кушал сидр акос любят все мы все умрем физтех чемпион физтех лучше всех ``` ``` # вставить пустую строку после первой строки > sed '1a\ ' test.txt мама мыла раму папа кушал сидр акос любят все мы все умрем физтех чемпион физтех лучше всех ``` ``` # заменить слова "мама" и "папа" на "родитель" > sed -E 's/(мама|папа) /родитель /' test.txt родитель мыла раму родитель кушал сидр акос любят все мы все умрем физтех чемпион физтех лучше всех ``` ``` # удалить все комментарии из Python-файлов текущего каталога # (без контроля синтаксиса, в том числе из строковых констант) > sed -i '/ *#/d' *.py ``` ``` # поменять местами два первых слова в каждой строке > sed -E 's/([а-я]+) ([а-я]+) (.*)/\2 \1 \3/' test.txt мыла мама раму кушал папа сидр любят акос все все мы умрем чемпион физтех физтех лучше всех # то же самое, то только для строк, начинающихся с буквы "м" > sed -E '/^[м].*/s/([а-я]+) ([а-я]+) (.*)/\2 \1 \3/' test.txt мыла мама раму папа кушал сидр акос любят все все мы умрем физтех чемпион физтех лучше всех ``` ================================================ FILE: practice/bpf/README.md ================================================ # Berkley Packet Filter Основной класс задач, в которых применяется чтение из RAW-сокетов, - это мониторинг системы. При этом возникает проблема большого потока данных, который необходимо обрабатывать, и постоянное переключение контекста между выполнением кода в пространстве ядра и в пространстве пользователя существенно снижает производительность. Поскольку принимать нужно не все пакеты, проходящие через сетевой интерфейс, а только те, которые соответствуют некоторым критериям, то логично перенести логику фильтрации в адресное пространство ядра, а затем получать от ядра только те пакеты, которые не отвергнуты фильтром. В качестве примера использования можно рассмотреть утилиту `tcpdump`, которая принимает в качестве аргумента текстовую строку, описывающую функцию фильтрации, и отображает только те события, которые соответствуют фильтру. В своей реализации утилита `tcpdump` использует BPF. Дополнительные материалы (English only): * Документация из поставки ядра Linux: [Linux Socket Filtering aka Berkley Packet Filter (BPF)](https://github.com/torvalds/linux/blob/v5.6/Documentation/networking/filter.txt) * [man 2 bpf ](http://man7.org/linux/man-pages/man2/bpf.2.html) * [BPF and XDP Reference Guide](https://cilium.readthedocs.io/en/latest/bpf/#bpf-and-xdp-reference-guide) ## Классический BPF: Linux Socket Filter ### Мониторинг сети и задача фильтрации Рассмотрим задачу фильрации пакетов на уровне Data Link Layer, и будем отлавливать те из них, которые соответствуют некоторому критерию. Для простоты можно рассмотреть IPv4/UDP-сообщения к определенному DNS-серверу, - такие запросы будет легко отлаживать. Создадим Data-Link сокет, и свяжем его с определенным сетевым интерфейсом, например `eth0`: ```c int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); /* Не забываем проверять ошибки! Packet-сокет невозможно создать, не имея права root или настроенный CAP_NET_RAW */ if (-1==sock) { perror("socket"); exit(1); } /* Далее нужно связать сокет с определенным сетевым интерфейсом */ struct ifreq req; memset(&req, 0, sizeof(req)); strncpy(req.ifr_name, "eth0", IFNAMSIZ); ioctl(sock, SIOCGIFINDEX, &req, sizeof(req)); // определяем индекс eth0 /* Для Data-Link Layer нужно заполнить только часть полей адреса, остальные должны быть инициализированы нулями */ struct sockaddr_ll addr; memset(&addr, 0, sizeof(addr)); addr.sll_family = AF_PACKET; // указываем, что структура для PACKET addr.sll_protocol = htons(ETH_P_ALL); // нас интересует только Ethernet addr.sll_ifindex = req.ifr_ifindex; // индекс устройства (см. выше) /* Привязываем сокет к определенному адресу. Если не вызывать bind, то нужно использовать recvfrom и sendto с явным указанием адреса */ if (-1==bind(sock, (struct sockaddr*)&addr, sizeof(addr))) { perror("bind"); exit(1); } ``` Теперь можно наблюдать за тем, что проходит через этот сетевой интерфейс. ```c for (;;) { char buffer[4096]; memset(buffer, 0, sizeof(buffer)); /* Читаем блок данных из сетевого устройства */ size_t cnt = recv(sock, buffer, sizeof(buffer), 0); uint32_t from_ip, to_ip; /* Извлекаем адреса источника и получателя из заголовка IPv4 */ memcpy(&from_ip, buffer+26, sizeof(from_ip)); memcpy(&to_ip, buffer+30, sizeof(to_ip)); char from_addr[20], to_addr[20]; memset(from_addr, 0, sizeof(from_addr)); memset(to_addr, 0, sizeof(to_addr)); inet_ntop(AF_INET, &from_ip, from_addr, sizeof(from_addr)); inet_ntop(AF_INET, &to_ip, to_addr, sizeof(to_addr)); printf("Got communication from %s to %s\n", from_addr, to_addr); } ``` На реально используемой системе можно будет наблюдать огромное количество пакетов сетевого взаимодействия. ### Виртуальная машина Classic-BPF Программа фильтрации, загружаемая в ядро, состоит из набора 64-битных RISC-команд, которые выполняются виртуальной машиной, либо могут быть транслированы в нативный код. Каждая инструкция кодируется следующим образом: ```c struct sock_filter { __u16 code; // 16 бит - код команды __u8 jt; // 8 бит - смещение для true/jump-инструкций __u8 jf; // 8 бит - смещение для false/jump-инструкций __u32 k; // 32 бит - поле для произвольных данных }; ``` У виртуальной машины есть только два 32-битных регистра: аккумулятор `A`, над которым можно выолнять произвольные действия, и счетчик инструкций `X`. Возможен доступ к "памяти", при этом адресуется содержимое исследуемого сетевого пакета. Поскольку виртуальная машина была спроектирована по аналогии с реально существующим процессором Motorola 6502, то для этого набора команд существует язык ассемблера. Программная реализация ассемблера BPF находится в поставке исходных текстах ядра Linux: `tools/bpf/bpf_asm`. #### Команды ассемблера BPF * Перемещение данных: - загрузить в регистр `A`: `ld` - слово, `ldh` - полуслово, `ldb` - байт; - сохранить значение в памяти: `st` для регистра `A`, `stx` для регистра `X`; - перемещение между регистрами: `tax` - из `A` в `X`, `txa` - из `X` в `A` * Арифметические операции над регистром `A`: `add`, `sub`, `mul`, `div`, `mod`, `neg`, `and`, `or`, `xor`, `lsh`, `rsh` * Переходы на метку: * `jmp` - безусловный переход; * `jeq`, `jne`, `jlt`, `jle`, `jgt`, `jge` - условный переход, при этом можно опционально указать вторую метку, на которую будет выполнен переход в случае не выполнения условия * Завершение работы: команда `ret` завершает работу и возвращает результат обработки фильтра. #### Пример программы на cBPF Рассмотрим задачу фильтрации Ethernet-фреймов: будем принимать только фреймы, внутри которых содержатся UDP-сообщения, адресованные DNS Google по адресу `8.8.8.8`. ```asm filter_google_dns: ldh [12] ; 16-бит значение после двух MAC-адресов jne #0x0800, fail ; проверяем, что внутри кадра у нас IPv4-пакет ldb [23] ; 8-бит значение типа протокола в заголовке IP jne #17, fail ; 17 - это UDP, 6 - это TCP ld [30] ; 4-байтное значение IP-адреса jne #0x08080808, fail ; сравниваем с адресом 8.8.8.8 success: ret #-1 ; значение -1 == 0xFFFFFFFF fail: ret #0 ; значение 0 ``` Данная программа проверяет, что внутри Ethernet-фрейма содержится действительно IPv4-пакет, который, в свою очередь, содержит сообщение типа UDP, и адресован получателю `8.8.8.8`. Возвращаемое значение - это максимальное количество байт, которое фильтр должен пропустить. Таким образом, значение `0` означает отклонение пакета, а максимально возможное беззнаковое целочисленное значение - пропуск пакета целиком. ### Загрузка программы-фильтра в ядро К сокету можно прикрепить BPF-фильтр, используя системный вызов `setsockopt`: ```c setsockopt( sock, // файловый дескриптор сокета SOL_SOCKET, // опция предназначена для сокета в целом SO_ATTACH_FILTER, // команда "присоединить фильтр" // указатель на структуру, которая содержит cBPF-программу struct *sock_fprog program, // размер аргумента; требуется как generic-параметр для setsockopt sizeof(struct sock_fprog) ); ``` Сама структура программы состоит из двух полей: указателя на последовательность инструкций, и количество инструкций (не байт!) в программе. Каждая инструкция - это 8 байт, которые можно описать структурой `struct sock_filter`. Ассемблер BPF, который имеется в составе исходных текстов ядра Linux, имеет опцию для вывода байткода в формате Си-структур, и этот вывод можно включить в код препроцессором. ```bash > linux-5.15/tools/bpf/bpf_asm -c filter.s >filter.inc ``` ```c struct sock_filter[] code = { #incldue "filter.inc" /* Здесь препроцессор вставит как есть текст вывода ассемблера: { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 5, 0x00000800 }, { 0x30, 0, 0, 0x00000017 }, { 0x15, 0, 3, 0x00000011 }, { 0x20, 0, 0, 0x0000001e }, { 0x15, 0, 1, 0x08080808 }, { 0x06, 0, 0, 0xffffffff }, { 0x06, 0, 0, 0000000000 }, */ }; struct sock_fprog program = { .filter = code, // указатель на последовательность инструкций /* Количество инструкций можно рассчитать как размер всех инструкций в байтах, деленный на размер одной инструкции */ .len = sizeof(code)/sizeof(code[0]) }; ``` Загрузка программы подразумевает её обязательную проверку верификатором, который проверяет, что: 1) все инструкции в программе корректны; 2) размер программы не превышает 4096 инструкций; 3) программа не содержит циклов. В случае отклонения программы верификатором, системный вызов `setsockopt` вернёт значение `-1`. ## Linux Extended BPF Начиная с ядра 3.19 в ядре Linux появилась новая реализация BPF, которая реализует виртуальную машину со следующими свойствами: * 11 регистров вместо 2, 10 из них - общего назначения * все регистры - 64-битные * появился стек, таким образом можно делать вложенные функции * вызовы некоторых встроенных функций в адресном пространстве ядра. Начиная с версии ядра 4.1, кроме возможности фильтрации сетевых пакетов, виртуальная машина EBPF позволяет выполнять код при наступлении определенных событий ядра. Кроме того, EBPF-программы могут использовать постоянные хранилища данных (`maps`), которые доступны также из адресного пространства процесса: массивы и хеш-таблицы. Таким образом, область применения расширилась до трассировки и измерения производительности. ### Системный вызов bpf ```c int bpf( // Команда взаимодействия с подсистемой EBPF int cmd, // Аргумент команды union bpf_attr *attr, // Размер аргумента команды unsigned int size ); ``` Использование системного вызова `bpf` на момент 02 апреля 2020 подразумевает использование функции `syscall`, поскольку Си-оболочка для него не реализована в `glibc`. Кроме того, многие возможности пока ещё не задокументированы. Основные команды для `bpf`: * `BPF_PROG_LOAD` - загрузить программу в ядро, и проверить её верификатором; возвращает дескриптор программы * `BPF_MAP_CREATE`, и другие команды `BPF_MAP_*` - создание постоянного хранилища, и операции над ним. Для каждой команды существует отдельная структура аргумента, описанная в ``. Программы, в свою очередь, могут быть разными по назначению: * `BPF_PROG_TYPE_SOCKET_FILTER` - простой фильтр, как в Classic BPF * `BPF_PROG_TYPE_KPROBE` - программа для обработки событий ядра `kprobe` * `BPF_PROG_TYPE_XDP` - продвинутая фильтрация пакетов как в файрволе * [и ещё много типов - см ``] Программа должна содержать функцию - точку входа, единственным аргументом которой, в регистре `R1`, будет указатель на контекст выполнения, тип которого зависит от типа программы. ### Загрузка программы Программа (возможно) загружается в ядро системы после вызова `bpf`: ```c /* Приходится использовать syscall, т.к. нет оболочки в glibc */ int program_id = syscall( SYS_bpf, // номер системного вызова bpf BPF_PROG_LOAD, // команда для загрузки программы &bpf_argument, // указатель на аргумент команды sizeof(bpf_argument) // ... и размер аргумента ); /* В случае успеха, будет возвращен файловый дескриптор программы. Для освобождения ресурсов, когда программа станет не нужна, нужно использовать обычный close() */ close(program_id); ``` Аргумент команды для загрузки - это структура (точнее, объявленная как `union`), в которой должны быть заполнены поля: ```c char bpf_code[4096*8] = ....; char loader_log[65535]; union bpf_attr bpf_argument = { .prog_type = BPF_PROG_TYPE_SOCKET_FILTER, // тип программы .insns = bpf_code, // указатель на бинарный код .insn_cnt = sizeof(bpf_code) / 8, // количество инструкций .log_level = 1, // вести ли лог загрузки? .log_buf = loader_log, // куда писать лог загрузки .log_size = sizeof(loader_log), // размер буфера для лога .license = "GPL" // должна быть GPL-совместимая }; ``` Лог загрузки содержит текст, который очень полезен при отладке, поскольку не любая программа будет считаться корректной с точки зрения верификатора. Загруженную программу, если это фильтр для сокета, можно прикрутить к сокету: ```c setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &program_id, sizeof(program_id)); ``` ### Байт-код EBPF-программ Формат EBPF программ отличается от Classic BPF, поэтому использовать старый ассемблер не получится. Формат инструкции: ```c struct bpf_insn { __u8 code, // 8-бит код команды __u8 dst_reg:4, // 4-бит регистр-назначение __u8 src_ref:4, // 4-бит регистр-источник __s16 off, // 16-бит значение для относительного адреса __s32 imm // 32-бит значение для кодирования констант }; ``` ### Компиляция EBPF-программ Компилировать EBPF-программы можно с помощью свежих версий тулкита CLang/LLVM. Необходимо проверить, что LLVM поддерживает цель `bpf`: ``` > llc --version LLVM (http://llvm.org/): LLVM version 9.0.1 Optimized build. Default target: x86_64-unknown-linux-gnu Host CPU: skylake Registered Targets: ...... bpf - BPF (host endian) ...... ``` Рассмотрим компиляцию тривиальной программы, которая запрещает все пакеты: ```c /* trivial.c */ int trivial_socket_filter(void *ctx) { return 0; // change to -1 to allow all } ``` Скомпилируем эту программу в объектный файл для цели `bpf`: ``` > clang -c -target bpf trivial.c ``` В результате получим объектный файл, который содержит примерно такой код: ``` > llvm-objdump -d trivial.o trivial.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 trivial_socket_filter: 0: 7b 1a f8 ff 00 00 00 00 *(u64 *)(r10 - 8) = r1 1: b7 00 00 00 00 00 00 00 r0 = 0 2: 95 00 00 00 00 00 00 00 exit ``` Нулевая инструкция загружает в стек переменную `ctx`, первая присваивает значение `r0 = 0` (по соглашению о вызовах, это возвращаемое значение), а вторая выполняет выход из функции. Для того, извлечь из объектного файла кусок кода, можно использовать утилиту `objcopy`. В дальнейшем этот код можно будет загрузить в программу как простые бинарные данные. ```bash > llvm-objcopy \ # требуется использовать objcopy из поставки LLVM -O binary \ # выходной формат - бинарный -j .text \ # копируем только секцию .text trivial.o \ # имя входного файла trivial.bin # имя файла с результатом ``` С помощью `objcopy` можно получить бинарный файл размером 24 байта, который содержит только код (ровно три инструкции), но не заголовки и таблицы. ``` > hexdump -C trivial.bin 00000000 7b 1a f8 ff 00 00 00 00 b7 00 00 00 00 00 00 00 |{...............| 00000010 95 00 00 00 00 00 00 00 |........| 00000018 ``` ### Ограничения на EBPF-программы Как и в случае с Classic BPF, загружаемые в ядро программы проходят строгую валидацию. Размер программы может составлять 4096 инструкций (до Linux 5.1), либо до миллиона инструкций (начиная с версии Linux 5.1). Так же валидатор проверяет, что программа гарантированно завершится за конечное время, поэтому использовать циклы в Си-программах можно только если на этапе компиляции известно число итераций. Для того, чтобы компилятор "развернул" циклы в длинную линейную программу, нужно перед циклом указать директиву `#pragma unroll` и указать опцию компиляции `-funroll-loops`. Функции стандартной Си-библиотеки становятся не доступны, поскольку программа выполняется в ограниченном окружении. Тем не менее, есть набор функций, доступных для EBPF-программ, они перечислены в [man 7 bpf-helpers](http://man7.org/linux/man-pages/man7/bpf-helpers.7.html). Ещё одним ограничением является порядок доступа к данным в памяти. Например, он должен быть выровненным, и вполне безобидная конструкция не пройдёт проверку валидатором: ```c ctx = 0; // начало пакета unsigned int ip_dest = *(unsigned int*)(ctx+30); // извлекаем IP-адрес ``` Вывод лога валидатора: ``` 6: (61) r1 = *(u32 *)(r1 +30) invalid bpf_context access off=30 size=4 ``` Поэтому приходится использовать низкоуровневые LLVM-функции для доступа к данным напрямую: ```c /* llvm builtin functions that eBPF C program may use to * emit BPF_LD_ABS and BPF_LD_IND instructions */ unsigned long long load_byte(void *skb, unsigned long long off) asm("llvm.bpf.load.byte"); unsigned long long load_half(void *skb, unsigned long long off) asm("llvm.bpf.load.half"); unsigned long long load_word(void *skb, unsigned long long off) asm("llvm.bpf.load.word"); ``` ## BPF Compiler Collection See tutorial: [bcc Python Developer Tutorial](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md) ================================================ FILE: practice/codestyle.md ================================================ # Coding Standards для языка Си ## Форматирование программ ### Отступы и скобочки Строгих требований к расстановке отступов нет. Какой стиль хотите использовать - тот и используйте. Но нужно придерживаться следующих правил: * Код не должен выглядеть страшно. * Ширина кода вашей программы не должна превышать 76 символов. Изначально это требование было продиктовано особенностями текстовых терминалов (80х25 минус рамочки редактора), но и в современной жизни, даже на больших экранах, часто бывает нужно расположить два текста сравнения, либо перегружать экран вспомогательнымии окнами (отладчики etc.). * Отступы должны быть кратны 2-м пробелам. * Нельзя смешивать отступы пробелами и символы табуляции. Используйте что-то одно. ### Объявление функций На языке Си, в отличии от С++ или Java, принято выносить отделение возвращаемых типов функций и их модификаторов от названия на отдельную строку. Например, так: ``` static void my_function(int arg1, char arg2, void *arg3) { /* function body */ } ``` Во-первых, типы возвращаемых значений бывают достаточно длинными (вроде `struct имя_структуры`), да ещё и модификаторы на некоторых платформах встречаются вида `extern __declspec(dllexport)`. В общем, тут трудно уложиться в положенные 76 символов. Во-вторых, это удобно при поиске функций по регулярному выражению `^имя_функции`, где символ `^` означает начало строки. ## Используемые типы данных Для знаковых целочисленных типов данных используются короткие имена: `char` (8 бит), `int` (как правило, 32 бит). Всё. Никаких `long long` или `short` использовать не нужно. Есть замечательный заголовочный файл ``, в котором перечислены типы данных с фиксированной точностью: `int8_t`, `int16_t`, `int32_t`, `int64_t`. Имена `char` и `int` являются общераспространенными синонимами для `int8_t` и `int32_t`, поэтому их использование не возбраняется за исключением особо экзотических случаев. А вот использование ключевого слова `long` - строго запрещено, поскольку на разных платформах и компиляторах его размер разный. Для беззнаковых типов данных нужно использовать только имена из ``: `uint8_t`, `uint16_t`, `uint32_t` и `uint64_t`. Обратите внимание, что `char` - это синоним понятия "байт", а вовсе не символ. ## Объявления констант Использование `#define` для объявления констант - строго запрещено! Целочисленные константы нужно объявлять как перечисления: ``` enum { CONST_VALUE_1 = 1, CONST_VALUE_2 = 567, CONST_VALUE_3 = 890 }; ``` Константы всех остальных типов данных - в стиле C++ по-Саттеру: ``` static const double PI = 3.14159; static const char *PATH = "/usr/local"; ``` ## Имена Самое главное - учите simplified English. За транслит нужно расстреливать! Но и не злоупотребляйте редко используемыми словами, подобранными через multitran.ru. Всё-таки не исключено, что ваш код будут читать индусы или китайцы, для которых английский также не является родным языком. На Си приняты `безумно_длинные_имена_функций_или_пар_модуль_подчеркивание_метод`, которые можно читать только в виде Python-style имен с подчеркиванием, но не `CamelStyle`. Для имен типов данных `CamelStyle` допустим, но не рекомендуется. Для типов данных обычно используется запись с суффиксом `_t`. Например: ``` struct MyStruct { int field_1; char field_2; }; typedef struct MyStruct my_struct_t; ``` Для переменных давайте понятные имена на английском языке; не нужно делать однобуквенные сокращения. Исключение - общепринятые однобуквенные целочисленные переменные: `N` (именно заглавная буква, ибо традиция), `i`, `j`, `k`. ## Используемые конструкции ### Оператор `goto` Всем известно, что оператор `goto` (дословный перевод на русский язык: `иди_на`) строго запрещён в промышленном программировании, а для языка Java - это вообще специально зарезервированное ключевое слово, которое приводит к ошибке компиляции. Но в системном программировании допускается единственный случай, когда использование `goto` целесообразно: завершение работы функции при обработке ошибок, когда требуется гарантированно освободить некоторые ресурсы. Пример: ``` int my_function() { char* memory = calloc(BUFFER_SIZE, sizeof(char)); int fd_read = open(PATH, O_RDONLY); int result = NO_ERROR; /* ... something unimportant ... */ if (/* error occured */ ) { result = SOME_ERROR_CODE; goto Function_End; } Function_End: free(memory); close(fd_read); return result; } ``` Во всех остальных случаях использовать оператор `goto` строго запрещено, как и в высокоуровневом программировании. ### Массивы переменного размера На языке Си, в отличии от C++, допускаются массивы переменного размера. Но использовать их можно только в том случае, если их размер заведомо предсказуем. Вот так допустимо: ``` enum {N = 100}; char array[N]; ``` Так тоже: ``` uint8_t N = /* some value, but not more 255 */; char array[N]; ``` А это - очень плохо: ``` int N; scanf("%d", N); /* what if 9999999999999999999 at input? */ char array[N]; ``` ================================================ FILE: practice/epoll/README.md ================================================ # Мультиплексирование ввода-вывода ## Неблокирующий ввод-вывод Системные вызовы `read` и `write` блокируют выполнение текущего потока выполнения в случае отсутствия данных или места в буферах ввода или вывода. В случае, когда процесс работает одновременно с несколькими файловыми дескрипторами, такое поведение может стать узким местом в производительности. Для того, чтобы избежать простоя на операциях ввода-вывода, предусмотрен аттрибут файлового дескриптора `O_NONBLOCK`, который можно установить как при открытии файла, так и для уже открытого файлового дескриптора с помощью системного вызова `fcntl`: ``` int fd = ....; // какой-то файловый дескриптор int flags = fcntl(fd, F_GETFL); // получить предыдущие флаги открытия/создания flags |= O_NONBLOCK; // добавить флаг неблокируемости fcntl(fd, F_SETFL, flags); // установить новые флаги ``` Попытка чтения из файлового дескриптора, если в буфере нет данных, или записи в файловый дескриптор, если в буфере нет места, приведет к ошибке. То есть, системный вызов `read` или `write` завершит работу со значением `-1`, и при этом в переменной `errno` будет зафиксировано значение `EAGAIN`. В этом случае, можно попробовать выполнить операцию ввода или вывода с файловым дескриптором позже, а тем временем обработать другие файловые дескрипторы. ## Обработка событий в Linux Если основное назначение программы - это обработка данных из файловых дескрипторов, то неблокирующий ввод-вывод может приводить к 100% загрузке процессора даже в том случае, когда с процессом не происходит никакого взаимодействия. Для того, чтобы это избежать, необходимо ставить процесс в состояние ожидания до тех пор, пока не возникнет какое-либо событие, связанное с одним из файловых дескрипторов, требующее реакции. Такой механизм является системно-зависимым (вне стандарта POSIX); в системе Linux он реализуется через механизм `epoll(7)`, в системах FreeBSD и MacOS - через системный вызов `kqueue`. Эти механизмы реализованы идентично, и различаются только в API. ### Очередь ядра Очередь ядра в системе Linux создается с помощью системного вызова `epoll_create` или `epoll_create1`. Результатом является файловый дескриптор (который должен быть закрыт, когда очередь станет не нужна), который связан с некоторым специальным объектом - очередью ядра, в которую складываются события по некоторому фильтру. Событие описывается структурой `epoll_event`: ``` #include typedef union { void* ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; // маска событий epoll_data_t data; // произвольное поле, заполненное при регистрации }; ``` Маска событий - это набор из различных флагов: * `EPOLLIN` - готовность к чтению; * `EPOLLOUT` - готовность к записи; * `EPOLLHUP` - соединение разорвано; * `EPOLLERR` - ошибка ввода-вывода. ### Ожидание событий Перевести процесс в ожидание поступления событий можно с помощью системного вызова `epoll_wait` или `epoll_pwait`. ``` struct epoll_event events[MaxEventsToRead]; int N = epoll_wait( int epoll_fd, // дескриптор, созданный epoll_create struct epoll_event *events, // куда прочитать события int MaxEventsToRead, // размер массива для чтения int timeout // таймаут в миллисекундах, или -1 ); ``` Системный вызов `epoll_wait` ожидает появление *хотя бы одного* события из зарегистрированных на наблюдение, блокируя выполнение текущего потока. Поскольку за время простоя процесса или потока может появиться несколько событий, то значение переменной `N` после выполнения `epoll_wait` может быть больше `1`, - это количество событий, которые были прочитаны в массив `events`. Если был указан параметр `timeout` в значение, отличное от `-1`, либо во время ожидания поступил сигнал, то значение `N` может стать равным `-1`, в `errno` будет записано значение `EINTR`. Системный вызов `epoll_pwait` дополнительно принимает ещё один аргумент - маску сигналов, которые нужно блокировать на время ожидания. После выполнения `epoll_wait`, в массив `events` будет записано `N` структур `epoll_event`, которые содержат описание того, что произошло с наблюдаемыми файловыми дескрипторами. При этом, если с одним и тем же файловым дескриптором произошло несколько событий, то они группируются в одно событие. ### Регистрация файловых событий для отслеживания изменений Для того, чтобы события, связанные с определенными файловыми дескрипторами, отслеживались и попадали в очередь ядра, их необходимо явно зарегистрировать с помощью `epoll_ctl`: ``` epoll_ctl( int epoll_fd, // дескриптор, созданный epoll_create int op, // одна из операций: // - EPOLL_CTL_ADD - добавить дескриптор в наблюдение // - EPOLL_CTL_MOD - модицифировать параметры наблюдения // - EPOLL_CTL_DEL - убрать дескриптор из наблюдения int fd, // дескриптор, события над которым нас интересуют struct epoll_event *event // структура, в которой описаны: // - маска интересуемых событий events // - произвольные данные, которые // as-is попадают в структуру, читаемую // epoll_wait ); ``` Добавить в один дескприптор `epoll` несколько раз один и тот же файловый дескриптор не получится, - это приведет к ошибке `EEXIST`. Если необходимо модицифировать маску наблюдаемых событий для уже зарегистрированного события, то нужно использовать операцию `EPOLL_CTL_MOD` вместо `EPOLL_CTL_ADD`. В том случае, если всё же необходимо определить разные обработчики событий для одного и того же файлового дескриптора, то можно создать его дубликат с помощью `dup2`. Если файловый дескриптор был закрыт, то он автоматически удаляется из наблюдаемых файловых дескприпторов. ### Отслеживание по изменению фронта (Edge-Triggerd) v.s. отслеживание по состоянию (Level-Triggered) Описание физической аналогии приведено в статье [Edge Triggered v.s. Level Triggered Interrupts](https://venkateshabbarapu.blogspot.com/2013/03/edge-triggered-vs-level-triggered.html) (на англ.). По умолчанию события регистрируются в режиме отслеживания по значению, то есть, по факту наличия флага готовности операций ввода и вывода, если в буфере есть готовые данные или место для записи. Регистрация обработки событий в режим Eddge-Triggered возможна с указанием флага `EPOLLET`. В этом случае повышается скорость реакции на событие за счет того, что событие регистрируется в тот момент, когда оно только начинает быть готовым, например в буфер ввода начали поступать данные. Недостатком этого подхода является то, что регистрируется только факт изменения состояния, и если, например, не прочитать данные из буфера полностью, то в следующий раз событие готовности к чтению не будет обнаружено, т.к. оно уже произошло. ## Событийно-ориентированное программирование Помимо ввода-вывода, в системе Linux могут быть использованы другие файловые дескрипторы специального назначения, которые также как и обычные, можно регистрировать в наблюдение через `epoll`. ### Пара виртуальных сокетов Пара виртуальных сокетов создается с помощью системного вызова `socketpair`, который, по аналогии с `pipe`, заполняет массив из двух целочисленных значений. Эти файловые дескрипторы могут быть использованы для взаимодействия родственных процессов, от отличаются от неименованных каналов тем, что: 1. Являются двунаправленными 2. Их можно настраивать через `setsockopts`, как обычные сокеты 3. Обрабатывается отдельная операция "завершения соединения", которая, в случае в `epoll` регистрируется как событие `EPOLLIN` с 0 количеством байт. ``` // Пример: int pair[2]; socketpair(AF_UNIX, // в Linux поддерживается только UNIX, SOCK_STREAM, // еще можно SOCK_DGRAM 0, // автоматический выбор протокола pair // массив из 2-х int, куда будут записаны дескрипторы ); ``` ### SignalFD, TimerFD, EventFD Системные вызовы `signalfd`, `timerfd_create`, и `eventfd` реализованы только в Linux, и создают специальные файловые дескрипторы, из которых можно читать события: * поступления определенных сигналов (`signalfd`); * срабатывание таймера (`timerfd_create`); * уведомления, пересылаемые разными потоками (`read` и `write` через `eventfd`). ================================================ FILE: practice/exec-rlimit-ptrace/README.md ================================================ # fork-МАГИЯ-exec ## Системный вызов exec Формальное описание системного вызова exec: [man 2 execve](http://ru.manpages.org/execve/2) Системный вызов `exec` предназначен для замены программы текущего процесса. Как правило, используется совместно с `fork`, но не обязательно. Си-оболочки для системного вызова `exec` имеют несколько разных сигнатур. ``` int execve(const char *filename, char *const argv[], char *const envp[]); int execvpe(.....) // параметры аналогично execve int execv(const char *filename, char *const argv[]) int execvp(......) // параметры аналогично execv int execle(const char *filename, const char arg0, ..., /* NULL */, const char env0, ..., /* NULL */); int execl(const char *filename, const char arg0, ..., /* NULL */); int execlp(......) // параметры аналогично execl ``` Различные буквы в суффиксах названий `exec` означают? * `v` или `l` - параметры передаются в виде массивов (`v`), заканчивающихся элементом `NULL`, либо в виде переменного количества аргументов (`l`), где признаком конца перечисления аргументов является значение `NULL`. * `e` - кроме аргументов программы передаются переменные окружения в виде строк `КЛЮЧ=ЗНАЧЕНИЕ`. * `p` - именем программы может быть не только имя файла, но и имя, которое нужно найти в одном из каталогов, перечисленных в переменной окружения `PATH`. Возвращаемым значением `exec` может быть только значение `-1` (признак ошибки). В случае успеха, возвращаемое значение уже в принципе не имеет никакого смысла, поскольку будет выполняться другая программа. Аргументы программы - это то, что передаётся в функцию `main` (на самом деле, они доступны из `_start`, поскольку располагаются на стеке). Первым аргументом (с индексом `0`), как правило, является имя программы, но это не является обязательным требованием. Классическим способом запуска новой программы является пара системных вызовов: ``` if (0 == fork) { execlp(program, program, NULL); perror("exec"); exit(1); } ``` Замена выполняемой программы с помощью `exec` оставляет неизменным многие аттрибуты процесса, например, открытые файловые дескрипторы, лимиты, и переменные окружения, установленные через `setenv`. Таким образом, между вызовами `fork` и `exec` можно провести дополнительную настройку программы перед выполнением. ``` if (0 == fork) { // Заменить стандартные потоки ввода/вывода на файлы // (имитация операции >ВЫХОД <ВХОД в bash) close(0); close(1); /* 0 = */ open(in_file, O_RDONLY); /* 1 = */ open(out_file, O_WRONLY|O_CREAT|O_TRUNC, 0640); execlp(program, program, NULL); perror("exec"); exit(1); } ``` Для того, чтобы случайно (в достаточно больших программах) не передать открытый файловый дескриптор новой программе, в системном вызове `open` предусмотрен флаг открытия `O_CLOEXEC`, который означает, что файл должен быть закрыт при вызове `exec`. ## Лимиты С процессом связаны некоторые лимиты (ограничения) на используемое процессорное время, максимальный объём памяти, количество файловых дескрипторов, процессов и т. д. Лимиты подразделяются на *жёсткие*, которые обычные пользователи могут только уменьшать (хотя `root` может и увеличивать), и *мягкие*, которые де-факто являются значениями по умолчанию, и их можно увеличить до жёсткого лимита. Примерами жёстких лимитов являются ограничения на количество процессов или объём доступной памяти. Пример мягкого лимита - это размер стека, который по умолчанию в Linux равен 8Мб, но может быть изменен произвольным образом, в том числе в большую сторону. Поскольку изменение размера стека - очень опасная операция, которая может нарушить структуру размещения данных в памяти, изменять этот лимит можно только до запуска функции `main`, либо непосредственно перед выполнением `exec`. В противном случае, поведение программы не определено. Получение и установка лимитов осуществляются с помощью системных вызовов `getrlimit` и `setrlimit`. Примеры лимитов см. в [get_limits.c](get_limits.c). Пример изменения размера стека см. в [shell_with_custom_stack_size.c](shell_with_custom_stack_size.c). ## Трассировка выполнения программы В Linux (не POSIX!) имеется системный вывов `ptrace`, назначение которого - обеспечить возможность отладки. Первым аргументом `ptrace` является команда, а дальше - некоторое количество аргументов команды. Если между вызовами `fork` и `exec` выполнить `ptrace(PTRACE_TRACEME,0,0,0)`, то процесс будет приостановлен до тех пор, пока к нему не подключится отладчик, либо программа, которая ведёт себя подобно отладчику, и не разрешит продолжить выполнение дальше. Некоторые команды, которые программа-"отладчик" может посылать исследуемому процессу: * `PTRACE_CONT, pid, 0, signo` - продолжить выполнение процесса. Если `signo` не 0, то отправляется сигнал. * `PTRACE_SINGLESTEP, pid, 0, 0` - выполнить одну инструкцию. * `PTRACE_SYSCALL, pid, 0, 0` - продолжить выполнение до системного вызова. * `PTRACE_GETREGS, pid, 0, &state)` - получить значение регистров процессора и сохранить в структуру `user_regs_state`, объявленную в файле ``. * `PTRACE_SETREGS, pid, 0, &state)` - модифицировать регистры процессора. * `PTRACE_PEEKDATA, pid, addr, 0` - прочитать одно машинное слово из памяти процесса по указанному адресу `addr`. * `PTRACE_POKEDATA, pid, addr, data` - записать одно машинное слово `data` в память процесса по указанному адресу `addr`. Пример перехвата системного вызова `write`, который цензурирует нехорошее английское слово в тексте вывода см. в [ptrace_catch_string.c](ptrace_catch_string.c). ================================================ FILE: practice/exec-rlimit-ptrace/get_limits.c ================================================ #include #include #include #include static void print_limit(int val, const char *name) { struct rlimit rlim; memset(&rlim, 0, sizeof(rlim)); getrlimit(val, &rlim); printf("%-15s│ ", name); if (RLIM_INFINITY==rlim.rlim_cur) printf("∞ │ "); else if (4==sizeof(void*)) printf("%-10u│ ", rlim.rlim_cur); else printf("%-10llu│ ", rlim.rlim_cur); if (RLIM_INFINITY==rlim.rlim_max) printf("∞\n"); else if (4==sizeof(void*)) printf("%-11u\n", rlim.rlim_max); else printf("%-11llu\n", rlim.rlim_max); } int main() { printf("───────────────┬───────────┬────────────\n"); printf(" Name │ Soft │ Hard \n"); printf("───────────────┼───────────┼────────────\n"); print_limit(RLIMIT_AS, "RLIMIT_AS"); print_limit(RLIMIT_CPU, "RLIMIT_CPU"); print_limit(RLIMIT_DATA, "RLIMIT_DATA"); print_limit(RLIMIT_FSIZE, "RLIMIT_LOCKS"); print_limit(RLIMIT_NICE, "RLIMIT_NICE"); print_limit(RLIMIT_NOFILE, "RLIMIT_NOFILE"); print_limit(RLIMIT_RSS, "RLIMIT_RSS"); print_limit(RLIMIT_STACK, "RLIMIT_STACK"); printf("───────────────┴───────────┴────────────\n"); } ================================================ FILE: practice/exec-rlimit-ptrace/ptrace_catch_string.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void premoderate_write_syscall(pid_t pid, struct user_regs_struct state) { size_t orig_buf = state.rsi; // ecx for i386 size_t size = state.rdx; // rdx for i386 char *buffer = calloc(size+sizeof(long), sizeof(*buffer)); int val = 0; for (size_t i=0; i #include #include #include #include #include #include #include int main(int argc, char *argv[]) { uint64_t stack_size = strtoull(argv[1], NULL, 10); stack_size *= 1024; // convert from KB to bytes pid_t pid = fork(); if (-1==pid) { perror("fork"); exit(1); } if (0==pid) { // set new limits before exec struct rlimit rlim; getrlimit(RLIMIT_STACK, &rlim); if (RLIM_INFINITY==rlim.rlim_max || rlim.rlim_max>stack_size) { rlim.rlim_cur = stack_size; } else { rlim.rlim_cur = rlim.rlim_max; } setrlimit(RLIMIT_STACK, &rlim); execlp("bash", "bash", NULL); /* perror("exec"); --- can't use stdlib after stack size change */ _exit(1); } else { int wstatus; waitpid(pid, &wstatus, 0); return WEXITSTATUS(wstatus); } } ================================================ FILE: practice/fdup-pipe/README.md ================================================ # Дублирование файловых дескрипторов. Каналы ## Дублирование файловых дескрипторов Системный вызов `fcntl` позволяет настраивать различные манипуляции над открытыми файловыми дескрипторами. Одной из команд манипуляции является `F_DUPFD` - создание *копии* дескриптора в текущем процессе, но с другим номером. Копия подразумевает, что два разных файловых дескриптора связаны с одним открытым файлом в процессе, и разделяют следующие его аттрибуты: * сам файловый объект; * блокировки, связанные с файлом; * текущая позиция файла; * режим открытия (чтение/запись/добавление). При этом, не сохраняется флаг `CLOEXEC`, который предпиывает автоматическое закрытие файла при выполнении системного вызова `exec`. Упрощённой семантикой для создания копии файловых дескрипторов являются системные вызовы POSIX: `dup` и `dup2`: ``` #include /* Возвращает копию нового файлового дескриптора, при этом, по аналогии с open, численное значение нового файлового дескриптора - минимальный не занятый номер. */ int dup(int old_fd); /* Создаёт копию нового файлового дескриптора с явно указанным номером new_fd. Если ранее файловый дескриптор new_fd был открыт, то закрывает его. */ int dup2(int old_fd, int new_fd); ``` ## Неименованные каналы Канал - это пара связанных между собой файловых дескрипторов, один из которых предназначен для только для чтения, а другой - только для записи. Канал создается с помощью системного вызова `pipe`: ``` #include int pipe(int pipefd[2]); ``` В качестве аргумента системному вызову `pipe` передается указатель на массив и двух целых чисел, куда будут записаны номера файловых дескрипторов: * `pipefd[0]` - файловый дескриптор, предназначенный для чтения; * `pipefd[1]` - файловый дескриптор, предназначенный для записи. ### Запись данных в канал Осуществляется с помощью системного вызова `write`, первым аргументом которого является `pipefd[1]`. Канал является буферизованным, под Linux обычно его размер 65К. Возможные сценарии поведения при записи: * системный вызов `write` завершается немедленно, если размер данных меньше размера буфера, и в буфере есть место; * системный вызов `write` приостанавливает выполнение до тех пор, пока не появится место в буфере, то есть предыдущие данные не будут кем-то прочитаны из канала; * системный вызов `write` завершается с ошибкой `Broken pipe` (доставляется через сигнал `SIGPIPE`), если с противоположной стороны канал был закрыт, и данные читать некому. ### Чтение данных из канала Осуществляется с помощью системного вызова `read`, первым аргументом которого является `pipefd[0]`. Возможные сценарии поведения при чтении: * если в буфере канала есть данные, то `read` читает их, и завершает свою работу; * если буфер пустой и есть **хотя бы один** открытый файловый дескриптор с противоположной стороны, то выполнение `read` блокируется; * если буфер пустой и все файловые дескрипторы с противоположной стороны каналы закрыты, то `read` немедленно завершает работу, возвращая `0`. ### Проблема dead lock При выполнении системных вызовов `fork`, `dup` или `dup2` создаются копии файловых дескрипторов, связанных с каналом. Если не закрывать все лишние (неиспользуемые) копии файловых дескрипторов, предназначенных для записи, то это приводит к тому, что при очередной попытке чтения из канала, `read` вместо того, чтобы завершить работу, будет находиться в ожидании данных. ``` int fds_pair[2]; pipe(fds_pair); if ( 0!=fork() ) // теперь у нас существует неявная копия файловых дескрипторов { // немного записываем в буфер static const char Hello[] = "Hello!"; write(fds_pair[1], Hello, sizeof(Hello)); close(fds_pair[1]); // а теперь читаем обратно char buffer[1024]; read(fds_pair[0], buffer, sizeof(buffer)); // получаем dead lock! } else while (1) shched_yield(); ``` Для того, чтобы избежать этой проблемы, необходимо тщательно следить за тем, в какие моменты создаются копии файловых дескрипторов, и закрывать их тогда, когда они не нужны. ================================================ FILE: practice/file_io/README.md ================================================ # Файловый ввод-вывод ## Файловые дескрипторы Файловые дескрипторы - это целые числа, однозначно идентифицирующие открытые файлы в рамках одной программы. Как правило, при запуске процесса дескрипторы `0`, `1` и `2` уже заняты стандартным потоком ввода (`stdin`), стандартным потоком вывода (`stdout`) и стандартным потоком ошибок (`stderr`). Файловые дескрипторы могут быть созданы с помощью операции создания или открытия файла. ## Системные вызовы open/close Системный вызов `open` предназначен для создания файлового дескриптора из существующего файла, и имеет формальную сигнатуру: ``` int open(const char *path, int oflag, ... /* mode_t mode */); ``` Первый параметр - имя файла (полное, или относительно текущего каталога). Второй параметр - параметры открытия файла, третий (опциональный) - права доступа на файл при его создании. Основные параметры открытия файлов: * `O_RDONLY` - только для чтения; * `O_WRONLY` - только на запись; * `O_RDWR` - чтение и запись; * `O_APPEND` - запись в конец файла; * `O_TRUNC`- обнуление файла при открытии; * `O_CREAT` - создание файла, если не существует; * `O_EXCL` - создание файла только если он не существует. В случае успеха возвращается неотрицательное число - дескриптор, в случае ошибки - значение `-1`. ## Обработка ошибок в POSIX Код ошибки последней операции хранится в глобальной целочисленной "переменной" `errno` (на самом деле, в современных реализациях - это макрос). Значения кодов можно определить из `man`-страниц, либо вывести текст ошибки с помощью функции `perror`. ## Аттрибуты файлов в POSIX В случае создания файла, обязательным параметром является набор POSIX-аттрибутов доступа к файлу. Как правило, они кодируются в восьмиричной системе исчисления в виде `0ugo`, где `u` - права доступа для владельца файла, `g` - права доступа для всех пользователей группы файла, `o` - для остальных. В восьмеричной записи значения от 0 до 7 соответствуют комбинации трёх бит: ``` 00: --- 01: --x 02: -w- 03: -wx 04: r-- 05: r-x 06: rw- 07: rwx ``` ## Чтение и запись в POSIX Чтение и запись осуществляются с помощью системных вызовов: ``` ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); ``` Здесь `buf` - указатель на буфер данных, а `count` - максимальное количество байт для чтения/записи. Как правило, в `count` указывается размер буфера данных при чтении, или количество данных при записи. Возвращаемый тип `ssize_t` - целочисленный, определенный в диапазоне `[-1...SSIZE_MAX]`, где `SSIZE_MAX` обычно совпадает с `SIZE_MAX/2`. Значение `-1` используется в качестве признака ошибки, неотрицательные значения - количество записанных/прочитанных байт, которое может быть меньше, чем `count`. Если системный вызов `read` вернул значение `0`, то достигнут конец файла, либо был закрыт канал ввода. ## Навигация по файлу в POSIX Если файл является обычным, то можно выполнять перемещение текущей позиции в файле. ``` off_t lseek(int fd, off_t offset, int whence); ``` Этот системный вызов предназначен для перемещения текущего указателя на файл. Третий параметр `whence` один из трех стандартных способов перемещения: * `SEEK_SET` - указать явным образом позицию в файле; * `SEEK_CUR` - сместить указатель на определенное смещение относительно текущей позиции; * `SEEK_END` - сместить указатель на определенное смещение относительно конца файла. Системный вызов `lseek` возвращает текущую позицию в файле, либо значение `-1` в случае возникновения ошибки. Тип `off_t` является знаковым, и по умолчанию 32-разрядным. Для того, чтобы уметь работать с файлами размером больше 2-х гигабайт, определяется значение переменной препроцессора **до подключения заголовочных файлов**: ``` #define _FILE_OFFSET_BITS 64 ``` В этом случае, тип данных `off_t` становится 64-разрядным. Определить значение переменных препроцессора можно не меняя исходные тексты программы, передав компилятору опцию `-DКЛЮЧ=ЗНАЧЕНИЕ`: ``` # Скомпилировать программу с поддержкой больших файлов gcc -D_FILE_OFFSET_BITS=64 legacy_source.c ``` ## Компиляция и запуск Windows-программ из Linux Для кросс-компиляции используется компилятор gcc с целевой системой `w64-mingw`. Устанавливается из пакета: * `mingw32-gcc` - для Fedora * `gcc-mingw-w64` - для Ubuntu * `mingw32-cross-gcc` - для openSUSE. Скомпилировать программу для Windows можно командой: ``` $ i686-w64-mingw-gcc -m32 program.c # На выходе получаем файл a.exe, а не a.out ``` Обратите внимание, что система Linux, в отличии от Windows различает регистр букв в файловой системе, поэтому нужно использовать стандартные заголовочные файлы WinAPI в нижнем регистре: ``` #include // правильно #include // скомпилируется в Windows, но не в Linux ``` Запустить полученный файл можно с помощью WINE: ``` $ WINEDEBUG=-all wine a.exe ``` Установка переменной окружения `WINEDEBUG` в значение `-all` приводит к тому, что в консоль не будет выводится отладочная информация, связанная с подсистемой `wine`, которая перемешивается с выводом самой программы. ## Файловые дескрипторы и другие типы данных в WinAPI В системе Windows для файловых дескрипторов используется тип `HANDLE`. Для однобайтных строк используется тип `LPCSTR`, для многобайтных - `LPCWSTR`. Функции WinAPI, которые имеют разные варианты поддержки строк, и работают с однобайтными функциями, заканчиваются на букву `A`, а функции, которые работают с многобайтными файлами - на букву `W`. Для беззнакового 32-разрядного числа, используемого для флагов - тип `DWORD`. Полный список типов данных [в документации от Microsoft](https://docs.microsoft.com/en-us/windows/desktop/winprog/windows-data-types). ## Функции WinAPI для работы с файлами Файл можно открыть с помощью функции [CreateFile](https://docs.microsoft.com/ru-ru/windows/desktop/api/fileapi/nf-fileapi-createfilea). Чтение и запись - с помощью функций [ReadFile](https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-readfile) и [WriteFile](https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-writefile). Навигация по файлу - с помощью функции [SetFilePointerEx](https://docs.microsoft.com/ru-ru/windows/desktop/api/fileapi/nf-fileapi-setfilepointerex). ================================================ FILE: practice/fork/README.md ================================================ # Процессы В UNIX-системах поддерживается многозадачность, которая обеспечивается: параллельным выполнением нескольких задач, изоляцией адресного пространства каждой задачи. Полный список процессов можно получить с помощью команды `ps -A`. Убить какой-то процесс можно с помощью команды `kill`. ## Свойства процессов У каждого процесса существует свои обособленные: * адресное пространство начиная с `0x00000000`; * набор файловых дескрипторов для открытых файлов. Кроме того, каждый процесс может находиться в одном из состояний: * работает (Running); * приостановлен до возникновения определенного события (Suspended); * приостановлен до явного сигнала о том, что нужно продолжить работу (sTopped); * более не функционирует, не занимает память, но при этом не удален из таблицы процессов (Zombie). Каждый процесс имеет свой уникальный идентификатор - Process ID (PID), который присваивается системой инкрементально. Множество доступных PID является ограниченным, и его исчерпание проводит к невозможности создания нового процесса (что является механизмом действия форк-бомбы). ## Иерархия процессов Между процессами существуют родственные связи "предок-потомок", таким образом, иерархия процессов представляет собой древовидную структуру. Корнем дерева процессов является процесс с PID=1, который называется `init` (в классических UNIX-системах, в том числе xBSD или консервативных Linux-дистрибутивах), либо `systemd`. Родительские процессы могут завершаться раньше, чем завершаются их потомки. В этом случае, осиротевшие процессы становятся прямыми потомками процесса с PID=1, то есть `init` или `systemd`. Процессы можно объединять в *группы процессов* (process group), - множества процессов, которым доставляются *сигналы* о некоторых событиях. Например, в одну группу могут объединяться все процессы, запущенные из одной вкладки приложения-терминала. Объединение нескольких групп процессов называется *сеансом* (session). Как правило, в сеансы объединяются группы процессов в рамках одного входа пользователя в систему (их может быть несколько, например, несколько входов по `ssh`). ## Системный вызов `fork` Создание нового процесса осуществляется с помощью системного вызова `fork`, который создаёт почти точную копию текущего процесса, причём оба процесса продолжают своё выполнение со следующей, после вызова `fork`, инструкции. Различить родительский процесс от его копии - дочернего процесса, можно по возвращаемому значению `fork`: для родительского процесса возвращается PID вновь созданного процесса, а для дочернего - число 0. ``` pid_t process_id; // в большинстве систем pid_t совпадает с int if ( -1 == ( process_id=fork() ) ) { perror("fork"); // ошибка создания нового процесса } else if ( 0 == process_id ) { printf("I'm a child process!"); } else { printf("I'm a parent process, created child %d", process_id); } ``` Ситуация, когда `fork` возвращает значение -1, как правило означает, что система исчерпала допустимый лимит ресурсов на создание новых процессов. Пример реализации форк-бомбы, назначение которой - исчерпать лимит на количество запущенных процессов: ``` #include #include #include #include #include #include int main() { char * are_you_sure = getenv("ALLOW_FORK_BOMB"); if (!are_you_sure || 0!=strcmp(are_you_sure, "yes")) { fprintf(stderr, "Fork bomb not allowed!\n"); exit(127); } pid_t pid; do { pid = fork(); } while (-1 != pid); printf("Process %d reached out limit on processes\n", getpid()); while (1) { sched_yield(); } } ``` ## Завершение работы процесса Любой процесс, при добровольном завершении работы, должен явным образом сообщить об этом системе с помощью системного вызова `exit`. При написании программ на языках Си или С++ этот системный вызов выполняется после завершения работы функции `main`, вызванной из функции `_start`, которая неявным образом генерируется компилятором. Обратите внимание, что в стандартной библиотеке языка Си уже существует одноименная функция `exit`, поэтому название Си-оболочки для системного вызова начинается с символа подчеркивания: `_exit`. Функция `exit` отличается от системного вызова `_exit` тем, что предварительно сбрасывает содержимое буферов вывода, а также последовательно вызывает все функции, зарегистрированные с помощью `atexit`. В качестве аргумента принимается целое число, - код возврата из программы. Несмотря на то, что код возврата является 32-разрядным, к нему применяется операция поразрядного "и" с маской `0xFF`. Таким образом, диапазон кодов возврата находится от 0 до 255. Код возврата предназначен для того, чтобы сообщить родительскому процессу причину завершения своей работы. ## Чтение кода возврата дочернего процесса Семейство системных вызовов `wait*` предназначено для ожидания завершения работы процесса, и получения информации о том, как процесс жил и умер. * `wait(int *wstatus)` - ожидание завершения любого дочернего процесса, возвращает информацию о завершении работы; * `waitpid(pid_t pid, int *wstatus, int options)` - ожидание (возможно неблокирующее) завершения работы конкретного процесса, возвращает информации о завершении работы; * `wait3(int *wstatus, int options, struct rusage *rusage)` - ожидание (возможно неблокирующее) завершения любого дочернего процесса, возвращает информацию о завершении работы и статистике использования ресурсов; * `wait4(pid_t pid, int *wstatus, int options, struct rusage *rusage)` - ожидание (возможно неблокирующее) завершения конкретного процесса, возвращает информацию о завершении работы и статистике использования ресурсов. Если в программе предусмотрено создание более одного дочернего процесса, то использовать системные вызовы `wait` и `wait3` настоятельно не рекомендуется, поскольку дочерние процессы могут завершать свою работу произвольным образом, и это может привести к неоднозначному поведению. Вместо них нужно использовать `waitpid` или `wait4`. Состояние возврата, которое можно прочитать из таблицы процессов после того, как процесс перестал функционировать, - это причина завершения работы, код возврата, если процесс завершился через `_exit`, или номер убившего его сигнала, если процесс был принудительно завершён. Это состояние закодировано в 32-битном значении, формат которого строго не определен стандартом POSIX. Для извлечения информации используется нобор макросов: * `WIFEXITED(wstatus)` - возвращает значение, отличное от 0, если процесс был завершен с помощью системного вызова `_exit`; * `WIFSIGNALED(wstatus)` - возвращает значение, отличное от 0, если процесс был завершен принудительно; * `WEXITSTATUS(wstatus)` - выделяет код возврата в диапазоне от 0 до 255; * `WTERMSIG(wstatus)` - выделяет номер сигнала, если процесс был завершён принудительно. Чтение кода возврата, - это не право, а обязанность родительского процесса. В противном случае, дочерний процесс, который завершил свою работу, становится процессом-зомби, информация о завершении которого продолжает занимать место в таблице процессов. Завершение работы родительского процесса при работающих дочерних приводит к тому, что код возврата будет прочитан процессом с PID=1, который автоматически становится родительским для "осиротевших" процессов. ================================================ FILE: practice/function-pointers/README.md ================================================ # Библиотеки функций и их загрузка ## Функции и указатели на них Код программ в системах с Фон-Неймановской архитектурой размещается в памяти точно так же, как и обычные данные. Таким образом, он может быть загружен или сгенерирован во время работы программы. Некоторые процессоры позволяют контролировать, какие участки памяти могут быть выполняемые, а какие - нет, и кроме того, это контролируется ядром. Таким образом, выполнить код можно только при условии, что он находится в страницах памяти, помеченных как выполняемые. ## Типизация указателей на функции Объявление вида ``` int (*p_function)(int a, int b); ``` интерптетируется следующим образом: `p_function` - это указатель на функцию, которая принимает два целочисленных аргумента, и возвращает целое знаковое число. Более общий вид указателя на функцию: ``` typedef ResType (*TypeName)(FuncParameters...); ``` Здесь `ResType` - возвращаемый тип целевой функции, `TypeName` - имя типа-указателя, `FuncParameters...` - параметры функции. Использование ключевого слова `typedef` является необходимым для языка Си, чтобы каждый раз не писать полностью тип (по аналогии со `struct`). Объявление указателей на функции необходимо для того, чтобы компилятор знал, как именно использовать адрес какой-то функции, и мог подготовить аргументы, и разбраться с тем, откуда брать возвращаемый результат функции. ## Библиотеки ELF-файл может быть не только исполняемым, но и библиотекой, содержащей функции. Библиотека отличается от исполняемого файла тем, что: * содержит таблицу доступных *символов* - функций и глобальных переменных (можно явно указать её создание опцией `-E`); * может быть размещена произвольным образом, поэтому программа обязана быть скомпилирована в позиционно-независимый код с опцией `-fPIC` или `-fPIE`; * не обязана иметь точку входа в программу - функции `_start` и `main`. Компиляция библиотеки производится с помощью опции `-shared`: ``` > gcc -fPIC -shared -o libmy_great_library.so lib.c ``` В Linux и xBSD для именования библиотек используется соглашение `libИМЯ.so`, для Mac - `libИМЯ.dynlib`, для Windows - `ИМЯ.dll`. Связывание программы с библиотекой подразумевает опции: * `-lИМЯ` - указыватся имя библиотеки без префикса `lib` и суффикса `.so`; * `-LПУТЬ` - указывается имя каталога для поиска используемых библиотек. ## Runtime Search Path При загрузке ELF-файла загружаются все необходимые библиотеки, от которых он явно зависит. Посмотреть список зависимостей можно с помощью команды `ldd`. Библиотеки располагаются в одном из стандартных каталогов: `/lib[64]`, `/usr/lib[64]` или `/usr/local/lib[64]`. Дополнительные каталоги для поиска библиотек определяются в переменной окружения `LD_LIBRARY_PATH`. Существует возможность явно определить в ELF-файле, где искать необходимые библиотеки. Для этого используется опция линковщика `ld -rpath ПУТЬ`. Для передачи опций `ld`, который вызывается из `gcc`, используется опция `-Wl,ОПЦИЯ`. В `rpath` можно указывать как абсолютные пути, так и переменную `$ORIGIN`, которая при загрузке программы раскрывается в каталог, содержащий саму программу. Это позволяет создавать поставку из программы и библиотек, которые не раскиданы по всей файловой системе: ``` > gcc -o program -L. -lmygreat_library program.c \ -Wl,-rpath -Wl,'$ORIGIN/'. ``` Это создаст выполняемый файл `program`, который использует библиотеку `libmy_great_library.so`, подразумевая, что файл с библиотекой находится в том же каталоге, что и сама программа. ## Загрузка библиотек во время выполнения Библиотеки можно не привязывать намертво к программе, а загружать по мере необходимости. Для этого используется набор функций `dl`, которые вошли в стандарт POSIX 2001 года. * `void *dlopen(const char *filename, int flags)` - загружает файл с библиотекой; * `void *dlsym(void *handle, const char *symbol)` - ищет в библиотеке необходимый символ, и возвращает его адрес; * `int dlclose(void *handle)` - закрывает библиотеку, и выгружает её из памяти, если она больше в программе не используется; * `char *dlerror()` - возвращает текст ошибки, связянной с `dl`. Если `dlopen` или `dlsym` не могут открыть файл или найти символ, то возвращается нулевой указатель. Пример использования - в файлах [lib.c](lib.c) и [dynload.c](dynload.c). ## Позиционно-независимый исполняемый файл Опция `-fPIE` компилятора указывает на то, что нужно сгенерировать позиционно-независимый код для `main` и `_start`, а опция `-pie` - о том, что нужно при линковке указать в ELF-файле, что он позиционно-независимый. Позиционно-независимый выполняемый файл в современных системах размещается по случайному адресу. Если позиционно-независимый исполняемый файл ещё и содержит таблицу экспортируемых символов, то он одновременно является и библиотекой. Если отсутствует опция `-shared`, то компилятор собирает программу, удаляя из неё таблицу символов. Явным образом сохранение таблицы символов задается опцией `-Wl,-E`. Пример: ``` # файл abc.c содержит int main() { puts("abc"); } > gcc -o program -fPIE -pie -Wl,-E abc.c # программа может выполняться как обычная программа > ./program abc # и может быть использована как библиотека > python3 >>> from ctypes import cdll, c_int >>> lib = cdll.LoadLibrary("./program") >>> main = lib["main"] >>> main.restype = c_int >>> ret = main() abc ``` ================================================ FILE: practice/function-pointers/dynload.c ================================================ #include #include #include typedef void (*func_t)(int); int main() { void * lib = dlopen("libmylib.so", RTLD_NOW); if (! lib) { fprintf(stderr, "dlopen error: %s\n", dlerror()); exit(1); } void * entry = dlsym(lib, "some_func"); if (! entry) { fprintf(stderr, "dlsym error: %s\n", dlerror()); exit(1); } func_t func = entry; func(123); dlclose(lib); } ================================================ FILE: practice/function-pointers/func-pointer.c ================================================ #include #include #include typedef double (*unary_real_function_t)(double); unary_real_function_t funcs[] = { &sqrt, exp, log, NULL }; int main() { double x = 100; unary_real_function_t func = NULL; double y; for (int i=0; funcs[i]; ++i) { func = funcs[i]; y = func(x); printf("func(%g) = %g\n", x, y); } } ================================================ FILE: practice/function-pointers/lib.c ================================================ #include void some_func(int a) { printf("%d\n", a); } ================================================ FILE: practice/function-pointers/main.c ================================================ extern void some_func(int x); #include int main() { some_func(123); fgetc(stdin); } ================================================ FILE: practice/fuse/README.md ================================================ # Реализация файловых систем без написания модулей ядра ## Общие сведения Файловые системы обычно реализуются в виде модулей ядра, которые работают в адресном пространстве ядра. Монтирование осуществляется командой [`mount(8)`](http://man7.org/linux/man-pages/man8/mount.8.html), которой необходимо указать: * *точку монтирования* - каталог в виртуальной файловой системе, в котором будет доступно содержимое смонтированной файловой системы; * *тип файловой системы* - один из поддерживаемых типов: `ext2`, `vfat` и др. Если не указать тип файловой системы, то ядро попытается автоматически определить её тип, но сделать это ему не всегда удаётся; * *устройство для монтирования* - как правило, блочное, устройство для монтирования реальных устройств, либо URI для сетевых ресурсов, либо имя файла для монтирования образа. Вызов команды `mount` без параметров отображает список примонтированных файловых систем. Постоянные файловые системы, которые монтируются при загрузке системы, перечислены в файле `/etc/fstab`, формат которого описан в [`fstab(5)`](http://man7.org/linux/man-pages/man5/fstab.5.html). Если точка монтирования указана в этом файле, то для монтирования файловой системы достаточно указать команде `mount` только точку монтирования. Некоторые типы файловых систем реализованы в виде сервисов, которые работают в пространстве пользователя, как обычные процессы. Ядро взаимодействует с ними, используя файл символьного устройства `/dev/fuse`. Когда ядру необходимо обслужить запрос к виртуальной файловой системе, то в случае, если точка монтирования содержит файловую систему FUSE, ядро формирует запрос в специальном формате, и отправляет его тому процессу, который открыл файл `/dev/fuse` и зарегистрировал открытый файловый дескриптор в качестве параметра системного вызова [`mount(2)`](http://man7.org/linux/man-pages/man2/mount.2.html). После этого процесс обязан сформировать ответ, который будет обработан модулем ядра `fuse.ko` и ядро выполнит запрошенную файловую операцию. Подсистема FUSE реализована в Linux и FreeBSD. ## Пример реализации - файловая система SSH Протокол SSH предназначен для терминального доступа к любой UNIX-системе, на которой запущен сервис `sshd`. Авторизация осуществляется через пароль, в этом случае нужно будет его ввести после запуска команды `ssh`, либо с использованием асимметричных RSA-ключей. Использование ключей обычно является более безопасным (при условии, что полностью запрещена авторизация по паролю), поскольку случайно подобранный ключ намного сложнее подобрать по словарю, чем пароль. Для создания пары ключей используется команда `ssh-keygen`, которая создает пару файлов `~/.ssh/id_rsa` и `~/.ssh/id_rsa.pub` , первый из которых является приватным ключом, а второй - публичным. Содержимое публичного ключа можно добавить отдельной строкой в текстовый файл `~/.ssh/authorized_keys` на целевом хосте, и после этого можно будет подключаться без ввода пароля. Для копирования ключа на удаленный сервер в большинстве дистрибутивов предусмотрен скрипт `ssh-copy-id`. С помощью ssh можно не только интерактивно взаимодействовать с удаленным хостом, но и выполнять отдельные команды, если указывать из последними аргументами. **Пример:** создание каталога на удаленном хосте и копирование в него файла с локального компьютера. ```bash # ssh user @ host "command to execute" > ssh victor@10.0.2.4 "mkdir ~/some_dir" # get contents pipe write contents to file > cat /bin/bash | ssh victor@10.0.2.4 "cat >~/some_dir/bash" # set file attributes > ssh victor@10.0.2.4 "chmod a+x ~/some_dir/bash" # ensure file has been copied > ssh ssh victor@10.0.2.4 "ls -l ~/some_dir/" total 1024 -rwxr-xr-x 1 victor victor 1012552 Apr 21 10:17 bash ``` Таким образом, используя ssh в сочетании со стандартными командами POSIX, можно реализовать произвольные файловые операции над удаленной файловой системой. Этот подход реализован в реализации файловой системы [sshfs](https://github.com/libfuse/sshfs). ```bash # local directory for remote contents > mkdir remote_host # mount user@host :path local mount point > sshfs victor@10.0.2.4:/ remote_host ``` При этом, реализация `sshfs` использует только `fork+exec` для запуска команды `ssh`, и работает в адресном пространстве пользователя, а не ядра, и использует обычные сторонние библиотеки: ```bash > ldd /usr/bin/sshfs linux-vdso.so.1 (0x00007ffff1985000) libfuse.so.2 => /lib64/libfuse.so.2 (0x00007f33e781e000) libgthread-2.0.so.0 => /usr/lib64/libgthread-2.0.so.0 (0x00007f33e761c000) libglib-2.0.so.0 => /usr/lib64/libglib-2.0.so.0 (0x00007f33e7305000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f33e70e7000) libc.so.6 => /lib64/libc.so.6 (0x00007f33e6d2d000) libdl.so.2 => /lib64/libdl.so.2 (0x00007f33e6b29000) libpcre.so.1 => /usr/lib64/libpcre.so.1 (0x00007f33e689c000) /lib64/ld-linux-x86-64.so.2 (0x00007f33e7c71000) ``` ## Библиотека libfuse ### Реализация демона FUSE Библиотека [libfuse](https://github.com/libfuse/libfuse) реализует функциональность взаимодействия с модулем ядра `fuse.ko` через файловый дескриптор файла `/dev/fuse`. Поскольку проект существует достаточно давно, программный интерфейс (API) библиотеки претерпел множество изменений, и перед включением заголовочного файла необходимо указать версию программного интерфейса, который будет использован: ```c #define FUSE_USE_VERSION 30 // API version 3.0 #include ``` В дальнейшем значение макроса `FUSE_USE_VERSION` будет использовано препроцессором при обработке файла `fuse.h` для условной подстановки соответствующих определенной версии сигнатур функций. До версии 3.0 изменений накопилось очень много, поэтому используется отдельная библиотека `libfuse3.so` вместо `libfuse.so`, в дальнейшем мы будем использовать именно её. Для FUSE не реализован пакет CMake, но существует описание в формате `pkg-config`, которое можно использовать из CMake: ```cmake find_package(PkgConfig REQUIRED) pkg_check_modules(FUSE REQUIRED fuse3) link_libraries(${FUSE_LIBRARIES}) # -lfuse3 -lpthread include_directories(${FUSE_INCLUDE_DIRS}) # -I/usr/include/fuse3 compile_options(${FUSE_CFLAGS_OTHER}) # empty since fuse 3.0 ``` Реализация файловой системы - это программа-демон, которая ожидает запросы от ядра и обслуживает их. Реализация тривиальной программы: ```c static struct fuse_operations operations = { // pointers to callback functions }; int main(int argc, char *argv[]) { // arguments to be preprocessed before passing to /sbin/mount.fuse3 struct fuse_args args = FUSE_ARGS_INIT(argc, argv); // run daemon int ret = fuse_main( args.argc, args.argv, // arguments to be passed to /sbin/mount.fuse3 &operations, // pointer to callback functions NULL // optional pointer to user-defined data ); return ret; } ``` Демон использует стандартный для монтирования набор аргументов командной строки, аналогичный команде [`mount.fuse(8)`](http://man7.org/linux/man-pages/man8/mount.fuse.8.html). Как минимум, требуется один позиционный аргумент - это точка монтирования. После выполнения операции монтирования, демон продолжает работать в фоновом режиме, используя `fork`. Для работы в текущем процессе (foreground) используется опция `-f`, что бывает полезно для отладки. Опция `-u` означает операцию отключения ранее зарегистрированной точки монтирования. Набор операций, который используется при обработке запросов от ядра может быть не полным, то есть не покрывать всю функциональность файловой системы, или даже пустым. В этом случае, при попытке обращения к файловой системе, возникнет ошибка. Так, для примера выше, команда `ls` завершит работу с ошибкой: ```bash # start empty FUSE implementation > ./my_filesystem work_dir # try to get filesystem contents > ls work_dir ls: cannot access 'work_dir': Function not implemented # umount filesystem and stop the daemon > fusermount3 -u work_dir ``` ### Реализация функциональности Реализация функциональности файловой системы определяется указателями на соответствующий функции в структуре `struct fuse_operations`, причем для большинства полей имена совпадают с именами соответствующих системных вызовов (за исключением системного вызова `stat`, поле которого называется `getattr`). Для большинства из функций возвращаемым значением является целое число: `0` в случае успеха, и отрицательное значение в случае ошибки. Значение модуля кода ошибки соответствует ожидаемому коду ошибки, который будет записан в `errno` после выполнения соответствующего системного вызова. Так, ошибке "файл не найден" соответствует возвращаемое значение `-ENOENT`, где значение константы `ENOENT` определено в заголовочном файле `` . **Полное описание** callback-функций доступно по ссылке: [https://libfuse.github.io/doxygen/structfuse__operations.html](https://libfuse.github.io/doxygen/structfuse__operations.html). #### Получение списка файлов Рассмотрим тривиальную файловую систему, которая содержит ровно два файла: `a.txt` и `b.txt` с одинаковым содержимым. Как минимум, необходимо иметь возможность узнать содержимое файловой системы, реализовав для этого [`readdir(2)`](http://man7.org/linux/man-pages/man2/readdir.2.html) для чтения содержимого каталога, и [`stat(2)`](http://man7.org/linux/man-pages/man2/stat.2.html) для получения атрибутов файлов, включая атрибуты самого корневого каталога, иначе будет невозможно узнать о том, что он действительно является каталогом, и тем самым - получить его содержимое. ```c // contents to be accessed by reading files static const char DummyData[] = "Hello, World!\n"; // callback function to be called after 'stat' system call int my_stat(const char *path, struct stat *st, struct fuse_file_info *fi) { // check if accessing root directory if (0==strcmp("/", path)) { st->st_mode = 0555 | S_IFDIR; // file type - dir, access read only st->st_nlink = 2; // at least 2 links: '.' and parent return 0; // success! } if (0!=strcmp("/a.txt", path) && 0!=strcmp("/b.txt", path)) { return -ENOENT; // error: we have no files other than a.txt and b.txt } st->st_mode = S_IFREG | 0444; // file type - regular, access read only st->st_nlink = 1; // one link to file st->st_size = sizeof(DummyData); // bytes available return 0; // success! } // callback function to be called after 'readdir' system call int my_readdir(const char *path, void *out, fuse_fill_dir_t filler, off_t off, struct fuse_file_info *fi, enum fuse_readdir_flags flags) { if (0 != strcmp(path, "/")) { return -ENOENT; // we do not have subdirectories } // two mandatory entries: the directory itself and its parent filler(out, ".", NULL, 0, 0); filler(out, "..", NULL, 0, 0); // directory contents filler(out, "a.txt", NULL, 0, 0); filler(out, "b.txt", NULL, 0, 0); return 0; // success } struct fuse_operations operations = { .readdir = my_readdir, // callback function pointer for 'readdir' .getattr = my_stat, // callback function pointer for 'stat' }; ``` Теперь реализация файловой системы позволяет получить содержимое каталога: ```bash > ./my_filesystem work_dir > ls -l work_dir total 0 -r--r--r-- 1 root root 15 Jan 1 1970 a.txt -r--r--r-- 1 root root 15 Jan 1 1970 b.txt # try to get file contents - still not implemented 'open' and 'read' > cat work_dir/a.txt cat: work_dir/a.txt: Function not implemented # umount filesystem and stop the daemon > fusermount3 -u work_dir ``` Обратите внимание, что даты создания файлов - 1 января 1970 года, - это соответствует значению 0 для формата времени в UNIX, а владелец файла и группа - пользователь и группа `root`, численные значения `uid` и `gid` которых равны 0. Эти поля могут быть также заполнены при реализации `stat`. Кроме того, утилита `ls` отображает `total 0`, поскольку это значение в выводе является количеством занятых блоков на диске, и эта информация отсутствует в атрибутах файлов. #### Чтение данных Для того, чтобы прочитать данные из файла, его нужно, как минимум, успешно открыть, и кроме того, реализовать функцию чтения, которая соответствует поведению системного вызова `read`. ```c // callback function to be called after 'open' system call int my_open(const char *path, struct fuse_file_info *fi) { if (0!=strcmp("/a.txt", path) && 0!=strcmp("/b.txt", path)) { return -ENOENT; // we have only two files in out filesystem } if (O_RDONLY != (fi->flags & O_ACCMODE)) { return -EACCES; // file system is read-only, so can't write } return 0; // success! } // contents of file static const char DummyData[] = "Hello, World!\n"; // callback function to be called after 'read' system call int my_read(const char *path, char *out, size_t size, off_t off, struct fuse_file_info *fi) { // 'read' might be called with arbitary arguments, so check them if (off > sizeof(DummyData)) return 0; // reading might be called within some non-zero offset if (off+size > sizeof(DummyData)) size = sizeof(DummyData) - off; const void *data = DummyData + off; // copy contents into the buffer to be filled by 'read' system call memcpy(out, data, size); // return value is bytes count (0 or positive) or an error (negative) return size; } // register functions as callbacks struct fuse_operations operations = { .readdir = my_readdir, .getattr = my_stat, .open = my_open, .read = my_read, }; ``` Теперь можно прочитать содержимое файла: ```bash > ./my_filesystem work_dir # get file contents - OK > cat work_dir/a.txt Hello, World! # try to create new file - still not implemented > touch work_dir/new_file.txt touch: cannot touch 'work_dir/new_file.txt': Function not implemented # umount filesystem and stop the daemon > fusermount3 -u work_dir ``` ### Опции монтирования У монтирования файловых систем могут быть опции, например точка монтирования и файл с образом, либо устройство, либо любой другой источник данных для файловой системы. Некоторые опции являются обязательными для всех FUSE-систем, например указание точки монтирования, а часть из них - быть специфичными для реализации определенных файловых систем. Функция, реализующая работу FUSE-демона `fuse_main` принимает два аргумента: количество опций `argc` и массив строк `argv`, по аналогии с функцией `main`. Если какие-то опции не распознаны `fuse_main`, либо их не достаточно для монтирования файловой системы, то эта функция завершается с ошибкой. Для выделения, специфичных для конкретной файловой системы, опций используется модифицируемый список опций `fuse_args`, который инициализируется макросом `FUSE_ARGS_INIT(argc, argv)`. Для извлечения специфичных опций по некоторым шаблонам используется функция `fuse_opt_parse`, которая принимает описания опций, которые необходимо распознать, выполняет разбор аргументов командной строки, и извлекает обработанные опции из массива `argv` структуры `fuse_args`, чтобы они потом не попали в `fuse_main`. Описанием одной опции является структура `fuse_opt`, которая содержит: * текстовую строку, содержащую шаблон аргумента, и возможно, форматную строку для значения, которое необходимо извлечь; * смещение в байтах относительно начала структуры, которую заполняет функция `fuse_opt_parse`, если параметр встретился среди аргументов командной строки; * целочисленное значение, которое будет записано в структуру; игнорируется в случае, если шаблон содержит формат значения, который нужно извлечь. Для разбора опций необходимо определить массив из структур `fuse_opt`, где последний элемент, по аналогии со строками, заполнен нулями, и вызвать `fuse_opt_parse`, передав указатель на структуру с опциями, которую необходимо заполнить по результатам их разбора. ```c int main(int argc, char *argv[]) { // initialize modificable array {argc, argv} struct fuse_args args = FUSE_ARGS_INIT(argc, argv); // struct to be filled by options parsing typedef struct { char *src; int help; } my_options_t; my_options_t my_options; memset(&my_options, 0, sizeof(my_options)); // options specifications struct fuse_opt opt_specs[] = { // pattern: match --src then string // the string value to be written to my_options_t.src { "--src %s", offsetof(my_options_t, src) , 0 }, // pattern: match --help // if found, 'true' value to be written to my_options_t.help { "--help" , offsetof(my_options_t, help) , true }, // end-of-array: all zeroes value { NULL , 0 , 0 } }; // parse command line arguments, store matched by 'opt_specs' // options to 'my_options' value and remove them from {argc, argv} fuse_opt_parse(&args, &my_options, opt_specs, NULL); if (my_options.help) { show_help_and_exit(); } if (my_options.src) { open_filesystem(my_options.src); } // pass rest options but excluding --src and --help to mount.fuse3 int ret = fuse_main(args.argc, args.argv, &operations, NULL); return ret; } ``` ================================================ FILE: practice/http-curl/README.md ================================================ # Протокол HTTP и библиотека cURL ## Протокол HTTP ### Общие сведения Протокол HTTP используется преимущественно браузерами для загрузки и отправки контента. Кроме того, благодаря своей простоте и универсальности, он часто используется как высокоуровневый протокол клиент-серверного взаимодействия. Большинтсво серверов работают с версией протокола `HTTP/1.1`, который подразумевает взаимодействие в текстовом виде через TCP-сокет. Клиент отправляет на сервер текстовый запрос, который содержит: * Команду запроса * Заголовки запроса * Пустую строку - признак окончания заголовков запроса * Передаваемые данные, если они подразумеваются В ответ сервер должен отправить: * Статус обработки запроса * Заголовки ответа * Пустую строку - признак окончания заголовков ответа * Передаваемые данные, если они подразумеваются Стандартным портом для `HTTP` является порт 80, для `HTTPS` - порт с номером 443, но это жёстко не регламентировано, и при необходимости номер порта может быть любым. ### Основные команды и заголовки HTTP * `GET` - получить содержимое по указанному URL; * `HEAD` - получить только метаинформацию (заголовки) по указанному URL, но не содержимое; * `POST` - отправить данные на сервер и получить ответ. Кроме основных команд, в протоколе HTTP можно определять произвольные дополнительные команды в текстовом виде (естественно, для этого потребуется поддержка как со стороны сервера, так и клиента). Например, расширение WebDAV протокола HTTP, предназначенное для передачи файлов, дополнительно определяет команды `PUT`, `DELETE`, `MKCOL`, `COPY`, `MOVE`. Заголовки - это строки вида `ключ: значение`, определяющие дополнительную метаинформацию запроса или ответа. По стандарту `HTTP/1.1`, в любом запросе должен быть как минимум один заголовок - `Host`, определяющий имя сервера. Это связано с тем, что с одним IP-адресом, на котором работает HTTP-сервер, может быть связано много доменных имен. Полный список заголовков можно посмотреть [в Википедии](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields). Пример взаимодействия: ``` $ telnet ejudge.atp-fivt.org $ telnet ejudge.atp-fivt.org 80 Trying 87.251.82.74... Connected to ejudge.atp-fivt.org. Escape character is '^]'. GET / HTTP/1.1 Host: ejudge.atp-fivt.org HTTP/1.1 200 OK Server: nginx/1.14.0 (Ubuntu) Date: Tue, 23 Apr 2019 21:18:43 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 4383 Connection: keep-alive Last-Modified: Mon, 04 Feb 2019 17:01:28 GMT ETag: "111f-58114719b3ca3" Accept-Ranges: bytes АКОС ФИВТ МФТИ ... ``` ### Протокол HTTPS Протокол HTTP**S** - это реализация протокола HTTP поверх дополнительного уровня SSL, который, в свою очередь работает через TCP-сокет. На уровне SSL осуществляется проверка сертификата сервера и обмен ключами шифрования. После этого - начинается обычное взаимодействие по протоколу HTTP в текстовом виде, но это заимодейтвие передается по сети в зашифрованном виде. Аналогом `telnet` для работы поверх SSL является инструмент `s_client` из состава OpenSSL: ``` $ openssl s_client -connect yandex.ru:443 ``` ### Утилита cURL Универсальным инструментом для взаимодействия по HTTP в Linux считается [curl](https://curl.haxx.se), которая входит в базовый состав всех дистрибутивов. Работает не только по протоколу HTTP, но и HTTPS. Основные опции `curl`: * `-v` - отобразить взаимодействие по протоколу HTTP; * `-X КОМАНДА` - отправить вместо `GET` произвольную текстовую команду в запросе; * `-H "Ключ: значение"` - отправить дополнительный заголовок в запросе; таких опций может быть несколько; * `--data-binary "какой-то текст"` - отправить строку в качестве данных (например, для `POST`); * `--data-binary @имя_файла` - отправить в качестве данных содержимое указанного файла. ## Библиотека `libcurl` У утилиты `curl` есть программный API, который можно использовать в качестве библиотеки, не запуская отдельный процесс. API состоит из двух частей: полнофункциональный асинхронный интерфейс (`multi`), и упрощённый с блокирующим вводом-выводом (`easy`). Пример использования упрощённого интерфейса: ``` #include CURL *curl = curl_easy_init(); if(curl) { CURLcode res; curl_easy_setopt(curl, CURLOPT_URL, "http://example.com"); res = curl_easy_perform(curl); curl_easy_cleanup(curl); } ``` Этот код эквивалентен команде ``` $ curl http://example.com ``` Дополнительные параметры, эквивалентные отдельным опциям команды `curl`, определяются функцией [`curl_easy_setopt`](https://curl.haxx.se/libcurl/c/curl_easy_setopt.html). Выполнение HTTP-запроса приводит к записи результата на стандартный поток вывода, но обычно бывает нужно получить данные для дальнейшей обработки. Это делается установкой одной из callback-функций, которая ответственна за вывод: ``` #include typedef struct { char *data; size_t length; } buffer_t; static size_t callback_function( char *ptr, // буфер с прочитанными данными size_t chunk_size, // размер фрагмента данных size_t nmemb, // количество фрагментов данных void *user_data // произвольные данные пользователя ) { buffer_t *buffer = user_data; size_t total_size = chunk_size * nmemb; // в предположении, что достаточно места memcpy(buffer->data, ptr, total_size); buffer->length += total_size; return total_size; } int main(int argc, char *argv[]) { CURL *curl = curl_easy_init(); if(curl) { CURLcode res; // регистрация callback-функции записи curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback_function); // указатель &buffer будет передан в callback-функцию // параметром void *user_data buffer_t buffer; buffer.data = calloc(100*1024*1024, 1); buffer.length = 0; curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); curl_easy_setopt(curl, CURLOPT_URL, "http://example.com"); res = curl_easy_perform(curl); // дальше можно что-то делать с данными, // прочитанными в buffer free(buffer.data); curl_easy_cleanup(curl); } } ``` ================================================ FILE: practice/ieee754/README.md ================================================ # Представление вещественных чисел Существует два способа представления вещественных чисел: с фиксированным количеством разрядов (fixed-point) под дробную часть, и с переменным числом разрядов (floating-point). Представление чисел с фиксированной точкой часто используется там, где требуется гарантированная точность до определенного разряда, например, в финансовой сфере. Представление в формате с плавающей точкой является более универсальным, и все современные архитектуры процессоров работают именно в этом формате. ## Числа с плавающей точкой в формате IEE754 Два основных типа вещественных с плавающей точкой, которые определены стандартом языка Си, - это `float` (используется 4 байта для хранения) и `double` (используется 8 байт). Самый старший бит в представлении числа - это признак отрицательного значения. Далее, по старшинству бит, хранится значения *смещенной экспоненциальной* части (8 бит для `float` или 11 бит для `double`), а затем - значение *мантиссы* (23 или 52 бит). Смещение экспоненциальной части необходимо для того, чтобы можно было в таком представлении хранить значения с отрицательной экспонентой. Смещение для типа `float` равно `127`, для типа `double` - `1023`. Таким образом, итоговое значение может быть получено как: ``` Value = (-1)^S * 2^(E-B) * ( 1 + M / (2^M_bits - 1) ) ``` где `S` - бит знака, `E` - значение смещенной экспоненты, `B` - смещение (127 или 1023), а `M` - значение мантиссы, `M_bits` - количество бит в экспоненте. ## Как получить отдельные биты вещественного числа Поразрядные операции относятся к целочисленной арифметике, и не предусмотрены для типов `float` и `double`. Таким образом, нужно сохранить вещественное число в памяти, и затем прочитать его, интерпретируя как целое число. В случае с языком C++ для этого предназначен оператор `reinterpret_cast`. Для языка Си есть два способа: использовать аналог `reinterpret_cast` - приведение указателей, либо использовать тип `union`. ### Приведение указателей ``` // У нас есть некоторое целое вещественное число, которое хранится в памяти double a = 3.14159; // Получаем указатель на это число double* a_ptr_as_double = &a; // Теряем информацию о типе, приведением его к типу void* void* a_ptr_as_void = a_ptr_as_void; // Указатель void* в языке Си можно присваивать любому указателю uint64_t* a_ptr_as_uint = a_ptr_as_void; // Ну а дальше просто разыменовываем указатель uint64_t b = *a_as_uint; ``` ### Использование типа `union` Тип `union` - это тип данных, который синтаксически очень похож на тип `struct`, то есть там можно перечислить несколько именованных полей, но концептуально - это совершенно разные типы данных! Если в структуре или классе, для хранения каждого поля для предусмотрено отдельное место в памяти, то для `union` этого не происходит, и все поля накладываются друг на друга при размещении в памяти. Обычно тип `union` используется в качестве вариантного типа данных (в С++ начиная с 17-го стандарта для этого предусмотрен `std::variant`), но в качестве побочного эффекта - его удобно использовать приведения типов в стиле `reinterpret_cast`, не используя при этом указатели. ``` // У нас есть некоторое целое вещественное число, которое хранится в памяти double a = 3.14159; // Используем тип union typedef union { double real_value; uint64_t uint_value; } real_or_uint; real_or_uint u; u.real_value = a; uint64_t b = u.uint_value; ``` ## Специальные значения в формате IEEE754 * Бесконечность: `E=0xFF...FF`, `M=0` * Минус ноль (результат деления 1 на минус бесконечность): `S=1`, `E=0`, `M=0` * NaN (Not-a-Number): `S` - любое, `E=0xFF...FF`, `M <> 0` Некоторые процессоры, например архитектуры x86, поддерживают расширение стандарта, позволяющее более эффективно представлять множество чисел, значения которых близко к нулю. Такие числа называются *денормализованными*. Признаком денормализованного числа является значение смещенной экспоненты `E=0`. В этом случае, численное значение получается следующим образом: ``` Value = (-1)^S * ( M / (2^M_bits - 1) ) ``` ### Значения Not-a-Number Некоторые процессоры, например Intel x86, различают два вида чисел `NaN` - невалидное значение. #### sNaN - Signaling NaN Значения `sNaN` возникают при выполнении операций, которые сигнализируют об ошибке на уровне прерывания процессора. Например, деление на `0`. Обычно, чтобы получить такие значения, необходимо собирать программу с опцией `-fno-signaling-nans`. Более подробно - см. [FloatingPointMath - GCC Wiki](https://gcc.gnu.org/wiki/FloatingPointMath) Пример битовой маски для типа `double` на x86_64, определяющей значение `sNaN`: ``` Номера битов 6 5 4 3 2 1 0 3210987654321098765432109876543210987654321098765432109876543210 ---------------------------------------------------------------- Значения 0111111111110100000000000000000000000000000000000000000000000000 ---------------------------------------------------------------- Регион SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM ``` #### qNaN - Quiet NaN В отличии от `sNaN`, значения Quiet NaN, которые получаются в результате вычислений, не приводят к прерыванию процессора и вызове обработчика исплючительной ситуации. Примером является попытка сложить `+inf` и `-inf`. Пример битовой маски для типа `double` на x86_64, определяющей значение `qNaN`: ``` Номера битов 6 5 4 3 2 1 0 3210987654321098765432109876543210987654321098765432109876543210 ---------------------------------------------------------------- Значения 0111111111111000000000000000000000000000000000000000000000000000 ---------------------------------------------------------------- Регион SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM ``` ================================================ FILE: practice/integers/README.md ================================================ # Целочисленная арифметика ## Целочисленные типы данных Минимально адресуемым размером данных является, какправило, один байт (8 бит). Как правило - это значит, что не всегда, и бывают разные экзотические архитектуры, где "байт" - это 9 бит (PDP-10), или специализированные сигнальные процессоры с минимально адресуемым размером данных 16 бит (TMS32F28xx). По стандарту языка Си определена константа `CHAR_BIT` (в заголовочном файле ``), для которой гарантируется, что `CHAR_BIT >= 8`. Тип данных, представляющий один байт, исторически называется "символ" - `char`, который содержит ровно `CHAR_BIT` количество бит. Знаковость типа `char` по стандарту не определена. Для архитектуры x86 это знаковый тип данных, а, например, для ARM - беззнаковый. Опции компилятора gcc `-fsigned-char` и `-funsigned-char` определяют это поведение. Для остальных целочисленных типов данных: `short`, `int`, `long`, `long long`, стандарт языка Си определяет минимальную разрядность: | Тип данных | Разрядность | | -----------| -------------------------------| | `short` | не менее 16 бит | | `int` | не менее 16 бит, обычно 32 бит | | `long` | не менее 32 бит | | `long long`| не менее 64 бит, обычно 64 бит | Таким образом, полагаться на количество разрядов в базовых типах данных нельзя, и это нужно проверять с помощью оператора `sizeof`, который возвращает "количество байт", то есть, в большинстве случает - сколько блоков размером `CHAR_BIT` помещается в типе данных. С особой осторожностью нужно относиться к типу данных `long`: на 64-разрядной системе Unix он является 64-битным, а, например, на 64-битной Windows - 32-битным. Поэтому, во избежание путаницы, использовать этот тип данных запрещено. ## Знаковые и беззнаковые типы данных Перед целочисленными типами данных могут стоять модификаторы `unsigned` или `signed`, которые указывают допустимость отрицательных чисел. Для знаковых типов, старший бит определяет знак числа: значение `1` является признаком отрицательности. Способ внутреннего представления отрицательных чисел не регламентирован [стандартом языка Си](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570), однако все современные компьютеры используют обратный дополнительный код. Более того, п.6.3.1.3.2 стандарта языка Си определяет способ приведения типов от знакового к беззнаковому таким способом, которые приводит к кодированию обратным дополнительным кодом. Таким образом, значение `-1` представляется как целое число, все биты которого равны единице. С точки зрения низкоуровневого программирования, и языка Си в частности, знаковость типов данных определяет только способ применения различных операций. ## Типы данных с фиксированным количеством бит В заколовочных файлах файле `` (для Си99+) и `` (для C++11 и новее) определены типы данных, для которых гарантируется фиксированное количесвто разрядов: `int8_t`, `int16_t`, `int32_t`, `int64_t`, - для знаковых, и `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` - для беззнаковых. # Переполнение Ситуация целочисленного переполнения возникает, когда тип данных результата не имеет достаточно разрядов для того, чтобы хранить итоговый результат. Например, при сложении беззнаковых 8-разрядных целых чисел 255 и 1, получается результат, который не может быть представим 8-разрядным значением. Для **беззнаковых чисел** ситуация переполнения является штатной, и эквивалентна операции "сложение по модулю". Для **знаковых** типов данных - приводит к ситуации *неопределенного поведения* (Undefined Behaviour). В корректных программах такие ситуации встречаться не могут. Пример: ``` int some_func(int x) { return x+1 > x; } ``` С точки зрения здравого смысла, такая программа должна всегда возвращать значение `1` (или `true`), поскольку мы знаем, что `x+1` всегда больше, чем `x`. Компилятор может использовать этот факт для оптимизации кода, и всегда возвращать истинное значение. Таким образом, поведение программы зависит от того, какие опции оптимизации были использованы. ## Контроль неопределенного поведения Свежие версии компиляторов `clang` и `gcc` (начиная с 6-й версии) умеют контролировать ситуации неопределенного поведения. Можно включить генерацию *управляемого* кода программы, который использует дополнительные проверки во время выполнения. Естественно, это происходит ценой некоторого снижения производительности. Такие инструменты называются *ревизорами* (sanitizers), предназначенными для разных целей. Для включения ревизора, контролирующего ситуацию неопределенного поведения, используется опция `-fsanitize=undefined`. ## Контроль переполнения, независимо от знаковости Целочисленное переполнение означает перенос старшего разряда, и многие процессоры, включая семейство x86, позволяют это диагностировать. Стандартами языков Си и C++ эта возможность не предусмотрена, однако компилятор gcc (начиная с 5-й версии) предоставляет **нестандартные** встроенные функции для выполнения операций с контролем переполнения. ``` // Операция сложения bool __builtin_sadd_overflow (int a, int b, int *res); bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res); bool __builtin_uadd_overflow (unsigned int a, unsigned int b, unsigned int *res); bool __builtin_uaddl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res); bool __builtin_uaddll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res); // Операция вычитания bool __builtin_ssub_overflow (int a, int b, int *res) bool __builtin_ssubl_overflow (long int a, long int b, long int *res) bool __builtin_ssubll_overflow (long long int a, long long int b, long long int *res) bool __builtin_usub_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_usubl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_usubll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) // Операция умножения bool __builtin_smul_overflow (int a, int b, int *res) bool __builtin_smull_overflow (long int a, long int b, long int *res) bool __builtin_smulll_overflow (long long int a, long long int b, long long int *res) bool __builtin_umul_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_umull_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_umulll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) ``` ================================================ FILE: practice/linux_basics/README.md ================================================ # Инструменты разработки на Си/С++ Предполагается, что на уровне пользователя вы знаете, что такое Linux. Если это не так, то необходимо прочитать [основы работы в Linux](intro.md). ## Компиляторы gcc и clang В стандартную поставку современных UNIX-систем входит один из компиляторов: либо `gcc`, либо `clang`. В случае с Linux, по умолчанию обычно используется `gcc`, а в BSD-системах - `clang`. Далее будет описана работа с компилятором `gcc`, имея ввиду, что работа с `clang` ничем принципиально не отличается: у обоих компиляторов много общего, в том числе опции командной строки. Кроме того, существует команда `cc`, которая является символической ссылкой на используемый по умолчанию компилятор языка Си (`gcc` или `clang`), и команда `c++`, - символическая ссылка на используемый по умолчанию компилятор для C++. Рассмотрим простейшую программу на языке C++: ``` // файл hello.cpp #include int main() { std::cout << "Hello, World!" << std::endl; return 0; } ``` Скомпилировать эту программу можно с помощью команды: ``` > c++ -o program.jpg hello.cpp ``` Опция компилятора `-o ИМЯ_ФАЙЛА` указывает имя выходного файла, который нужно создать. По умолчанию используется имя `a.out`. Обратите внимание, что файл `program.jpg` является обычным выполняемым файлом! ### Стадии сборки программы на Си или C++ При выполнении команды `c++ -o program.jpg hello.cpp` выполняется достаточно сложная цепочка действий: 1. Выполняется *препроцессинг* текстового файла `hello.cpp`. На этом этапе обрабатываются *директивы препроцессора* (которые начинаются с символа `#`), и получается новый текст программы. Если запустить компилятор с опцией `-E`, то будет выполнен только этот шаг, и на стандартный поток вывода будет выведен преобразованный текст программы. 2. Выполняется *трансляция* одного или нескольких текстов на Си или C++ в объектные модули, содержащие машинный код. Если указать опцию `-c`, то на этом сборка программы будет приостановлена, и будут созданы объектные файлы с суффиксом `.o`. Объектные файлы содержат *бинарный* исполняемый код, которому в точности соотвествует некоторый текст на языке ассемблера. Этот текст можно получить с помощью опции `-S`, - в этом случае, вместо объектных файлов будут созданы текстовые файлы с суффиксом `.s`. 3. Компоновка одного или нескольких объектных файлов в исполняемый файл, и связываение его со стандартной библиотекой Си/С++ (ну и другими библиотеками, если требуется). Для выполнения компоновки компилятор вызывает стороннюю программу `ld`. ### Программы на Си v.s. программы на C++ Компилятор `gcc` имеет опцию `-x ЯЗЫК`, для указания языка исходного текста программы: Си (`c`), C++ (`c++`) или Фортран (`fortran`). По умолчанию, язык исходного текста определяется в соответствии с именем файла: `.c`, - это программы на языке Си, а файлы, оканчивающиеся на `.cc`, `.cpp` или `.cxx`, - это тексты на языке C++. Таким образом, имя файла является существенным. Это относится к стадиям препроцессинга и трансляция, но может вызвать проблемы на стадии компоновки. Например, используя команду `gcc` вместо `g++` (или `cc` вместо `c++`), можно успешно скомпилировать исходный текст программы на C++, но при этом возникнут ошибки на стадии связывания, поскольку компоновщику `ld` будут переданы опции, подразумевающие связывание только со стандартной библиотекой Си, но не C++. Поэтому, при сборке программ на C++ нужно использовать команду `c++` или `g++`. ### Указание стандартов Опция компилятора `-std=ИМЯ` позволяет явным образом указать используемый стандарт языка. Рекомендуется явным образом указывать используемый стандарт, поскольку поведение по умолчанию зависит от используемого дистрибутива Linux. Допустимые имена: * `c89`, `c99`, `c11`, `gnu99`, `gnu11` для языка Си; * `c++03`, `c++11`, `c++14`, `c++17`, `gnu++11`, `gnu++14`, `gnu++17` для языка C++. Двузначное число в имени стандарта указывает на его год. Если в имени стандарта присутствует `gnu`, то подразумеваются GNU-расширения компилятора, специфичные для UNIX-подобных систем, и кроме того, считается определенным макрос `#define _DEFAULT_SOURCE`, который в некоторых случаях меняет поведение отдельных функций стандартной библиотеки. В дальнейшем мы будем ориентироваться на стандарт `c11`, а в некоторых задачах, где будет про это явно указано - его расширением `gnu11`. ### Директивы препроцессора Через опции командной строки можно определять константы, которые обрабатывается на стадии препроцессинга текстового файла. Для этого используется опция `-DИМЯ=ЗНАЧЕНИЕ`. В процессе компиляции эти имена эквивалентны конструкциям `#define`. ``` #ifdef PREDEFINED_CONSTANT static const char * StringConstant = PREDEFINED_CONSTANT; #else static const char * StringConstant = "Default value"; #endif ``` Данный код может быть скомпилирован с опцией, которая доопределит `PREDEFINED_CONSTANT`: ``` > cc -DPREDEFINED_CONSTANT='"Hello, World!"' ``` Обратите внимание, что если необходимо использовать символы пробелов или кавычек, то их необходимо либо экранировать, либо заключить аргумент в одинарные кавычки. ## Объектные файлы, библиотеки и исполняемые файлы ### Модуль ctypes интерпретатора Python Рассмотрим программу на языке Си: ``` /* my-first-program.c */ #include static void do_something() { printf("Hello, World!\n"); } extern void do_something_else(int value) { printf("Number is %d\n", value); } int main() { do_something(); } ``` Скомпилируем эту программу в объектный файл, а затем - получим из него: (1) выполняемую программу; (2) разделяемую библиотеку. Обратите внимание на опцию `-fPIC`, предназначенную для генерации позиционно-независимого кода, о чем будет рассказано на одном из последующих семинарах. ``` > gcc -c -fPIC my-first-program.c > gcc -o program.jpg my-first-program.o > gcc -shared -o library.so my-first-program.o ``` В результате мы получим программу `program.jpg`, которая выводит на экран строку `Hello, World!`, и *библиотеку* с именем `library.so`, которую можно использовать как из Си/C++ программы, так и динамически подгрузить для использования интерпретатором Python: ``` > python3 Python 3.6.5 (default, Mar 31 2018, 19:45:04) [GCC] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from ctypes import cdll >>> lib = cdll.LoadLibrary("./library.so") >>> lib.do_something_else(123) >>> retval = lib.do_something_else(123) Number is 123 >>> print(retval) 14 ``` Обратите внимание, что результатом работы функции `do_something_else` является какое-то загадочное число `14` (возможно, будет какое-то другое при попытке воспроизвети этот эксперимент), хотя функция возвращает `void`. Причина заключается в том, что разделяемые библиотеки хранят только **имена** функций, но не их сигнатуры (типы параметров и возвращаемого значения). Попытка вызвать функцию `do_something` не увенчается успехом: ``` >>> lib.do_something() Traceback (most recent call last): File "", line 1, in File "/usr/lib64/python3.6/ctypes/__init__.py", line 361, in __getattr__ func = self.__getitem__(name) File "/usr/lib64/python3.6/ctypes/__init__.py", line 366, in __getitem__ func = self._FuncPtr((name_or_ordinal, self)) AttributeError: ./library.so: undefined symbol: do_something ``` В этом случае имя `do_something` не найдено, поскольку в исходном тексте на языке Си модификатор `static` перед именем функции явно запрещает использование функции где-либо вне текущего исходного текста. ### Просмотр таблицы символов Для исследования объектных файлов, в том числе и скомпонованных, используется утилита `objdump`. Опция `--syms` или `-t` отображает отдельные секции исполняемого объектного файла, которым присвоены имена - *символы*. Некоторые имена имеют пометку `*UND*`, - это означает, что имя используется в объектном файле, но его расположение неизвестно. Задача компоновщика состоит как раз в том, чтобы найти требуемые имена в разных объектных файлах или динамических библиотеках, а затем - подставить правильный адрес. Некоторые символы помечены как глобальные (символ `g` во втором столбце вывода), а некоторые - как локальные (символ `l`). Те символы, которые не являются глобальными, считаются *не экспортируемыми*, то есть (теоретически) не должны быть доступны извне. ## Автоматизация сборки с помощью Makefile При разработке больших проектов, состоящих из нескольких файлов, возникает необходимость в автоматизации процесса сборки. Автоматизацией сборки занимается утилита `make`, которая использует описание сборки `Makefile`, специфичный для целевой и операционной системы и заведомо известной конфигурации. В простых случаях эти описания можно написать самостоятельно, но в более сложных, когда файлов много, или процесс сборки зависит от параметров, используется предварительная генерация файла `Makefile` с помощью более высокоуровневого инструмента, например `cmake`. Общий формат минимального `Makefile`: ``` # могут быть комментарии имя_цели: список зависимостей команда 1 команда 2 ... команда N ``` Обратите внимание, что отступы перед именами команд - это строго символы табуляции, а не последовательности пробелов. Текстовые редакторы, ориентированные на написание исходного кода, обычно при работе с `Makefile` игнорируют пользовательские настройки, связанные с отступами, и используют символ табуляции. Зависимостями могут быть определенные файлы исходных текстов, либо файлы промежуточного этапа компиляции, которые сами являются другими целям сборки. При запуске утилиты `make` проверяются даты модификации файлов, перечисленных в списке зависимостей, и выполняются команды только для тех целей, которые действительно требуют пересборки. Целей может быть несколько, причем имена целей не обязаны совпадать с именами генерируемых ими файлов. При запуске `make` без параметров выполняется сборка той цели, которая описана самой первой, поэтому часто выделяют отдельную цель под названием `first`. Кроме того, предусматривают отдельную цель под названием `clean`, которая должна оставить после себя только конечный результат, удалив все временные файлы. Команда `make` может использовать разные компиляторы и дополнительные параметры, которые можно переопределять через аргументы командной строки. В самом файле эти параметры используются как переменные с синтаксисом `$(имя_переменной)`. Пример `Makefile` для программы и бибилиотеки, которые были приведены выше: ``` CC=gcc # компилятор по умолчанию LINK=gcc # линковщик по умолчанию CFLAGS=-fPIC # флаги компилятора по умолчанию # первая цель для сборки first: all # виртуальная цель для сборки всего all: library.so program.jpg # библиотека library.so зависит от my-first-program.o library.so: my-first-program.o $(LINK) -shared -o library.so my-first-program.o # программа тоже зависит от объектного файла program.jpg: my-first-program.o $(LINK) -o program.jpg my-first-program.o # правило для сборки объектного файла my-first-program.o: my-first-program.c $(CC) -c $(CFLAGS) my-first-program.c # правило для очистки проекта clean: # обратите внимание на || true # - подавляет ошибку, если файлы не существуют rm -rf *.o || true # правило для очистки всего distclean: clean rm -rf program.jpg library.so || true ``` Если требуется во время сборки определить какие-либо параметры окружения, то можно вызывать внешние команды, и использовать их вывод в качестве переменных. Для этого используется конструкция `ПЕРЕМЕННАЯ=$(shell КОМАНДА)`. Кроме того, все реализации `make` поддерживают примитивные конструкции для проверки условий `ifeq (A, B) ... endif`. ``` # пример сборки с учетом имени операционной системы # команда uname возвращает имя операционной системы UNAME=$(shell uname -s) ifeq ($(UNAME), Darwin) OS=APPLE endif ifeq ($(UNAME), Linux) OS=LINUX endif platform_specific.o: platform_specific.c $(CC) -c $(CFLAGS) -D__$(OS)__ platform_specific.c ``` ``` // platform_specific.c #ifdef __APPLE__ // эта часть будет компилироваться только под Mac #endif #ifdef __LINUX__ // эта часть будет компилироваться только под Linux #endif ``` ## Проверка стиля кода Перед публикацией кода, не важно, куда - в репозиторий или при отправке решений, необходимо следовать единому стилю кода, про который есть договоренности с теми, кто этот код будет изучать. Для автоматизации этого процесса используется утилита `clang-format`. Для языков семейства Си нет единого стандарта, как должен быть отформатирован код. Есть несколько сложившихся в Open Source стандартов, которые задаются опцией `--style=ИМЯ`: `LLVM`, `GNU`, `Google`, `Chromium`, `Microsoft`, `Mozilla`, `WebKit`. Если указать опцию `--style=file`, то стандарт оформления будет использован из локального файла `.clang-format` (имя начинается с символа точки). Файл `.clang-format` может располагаться не только в одном каталоге с исходным файлом, но и в любом каталоге выше по иерархии, - в этом случае будет использован первый найденный файл. По умолчанию утилита `clang-format` выводит преобразованный исходный файл на стандартный поток вывода, опция `-i` (inplace) указывает, что необходимо перезаписать исходный файл. Некоторые тектовые редакторы и среды разработки позволяют интегрировать `clang-format` для переопределения стиля форматирования текста. Для редактора Emacs необходимо установить пакет `clang-format`, и дописать в конфигурационный файл `~/.emacs` строчки: ``` (require 'clang-format) ;; загрузка пакета clang-format (setq clang-format-style "file") ;; опция --style=file (global-auto-revert-mode t) ;; автозагрузка изменений файла ``` После этого код можо переформатировать командой `cla-b`. Среда разработки CLion начиная с версии 2021.2 определяет наличие файла `.clang-format`, и позволяет подключить переопределение стиля кода. Для этого нужно в правом нижнем углу окна кликнуть на количество пробелов, и выбрать из всплывающего меню элемент "ClangFormat". clion-clang-format Переформатирование кода в CLion выполняется из меню Code > Reformat Code. ## Использование отладчика Если скомпилировать программу с опцией `-g`, то размер программы увеличится, поскольку в ней появляются дополнительные секции, которые содержат *отладочную информацию*. Отладочная информация содержит сведения о соответствии отдельных фрагментов программы исходным текстам, и включает в себя номера строк, имена исходных файлов, имена типов, функций и переменных. Эти информация используется только отладчиком, и почти никак не влияет на поведение программы. Таким образом, отладочную информацию можно совмещать с оптимизацией, в том числе достаточно агрессивной (опция компилятора `-O3`). Для запуска программы под отладчиком используется команда `gdb`, в качестве аргумента к которой указывается имя выполняемого файла или команды. Основные команды `gdb`: * `run` - запуск программы, после `run` можно указать аргументы; * `break` - установить точку останова, параметрами этой команды может имя функции или пара `ИМЯ_ФАЙЛА:НОМЕР_СТРОКИ`; * `ni`, `si` - шаг через строку или шаг внутрь функции соответственно; * `return` - выйти из текущей функции наверх; * `continue` - продолжить выполнение до следующей точки останова или исключительной ситуации; * `print` - вывести значение переменной из текущего контекста выполнения; * `layout src|regs|split` - переключение вида консольного отладчика; * `focus src|regs` - переключениние фокуса клавиш стрелок между разными окнами. Взаимодействие с отладчиком производится в режиме командной строки. Различные интегрированные среды разработки (CLion, CodeBlocks, QtCreator) являются всего лишь графической оболочкой, использующей именно этот отладчик, и визуализируя взаимодействие с ним. Более подробный список команд можно посмотреть в [CheatSheet](https://www.cheatography.com/fristle/cheat-sheets/closed-source-debugging-with-gdb/). ### Удаленная отладка Отладчик `gdb` может запускаться без интерфейса, что удобно в следующих случаях: * явное разделение ввода-вывода и команд отладчика; * запуск программы на удаленном компьютере через SSH, и управление отладчиком с минимальными задержками на медленном соединении; * использование GUI для управления отладчиком. Для запуска программы под удаленным отладчиком используется команда `gdbserver`: ``` > gdbserver localhost:12345 ./my_program ARG1 ARG2 output ``` Первым параметром `gdbserver` указывается `ХОСТ:ПОРТ`, где `localhost` означает имя компьютера в сети (его может и не быть, но имя `localhost` определено всегда), а номер порта должен быть в диапазоне от 1000 до 65535, и порт должен быть не занят чем-то еще. После запуска `gdbserver` программа НЕ запускается, а только загружается отладчиком, и сразу же переходит в режим паузы. Для подключения к серверу отладки нужно запустить отладчик `gdb` и выполнить команду: ``` (gdb) target remote localhost:12345 ``` Для того, чтобы лишний раз не передавать бинарные файлы с отладочной информацией по сети, их можно предварительно загрузить из локальной копии с помощью команды `file`: ``` (gdb) file ./my_program ``` После того, как установлено подключение к `gdbserver`, программа все еще не выполняется и находится в режиме паузы, и запустить ее на выполнение можно с помощью команды `continue`. В случае использования другого компьютера, а не локального хоста, дополнительный порт часто бывает не доступен, например он закрыт файрволом. В этом случае можно (и даже нужно по соображениям безопасности) использовать утилиту `ssh` для туннелирования дополнительных портов: ``` # подключаемся по ssh к remote-server # удаленный порт 12345 будет доступен как локальный порт 56789 local-pc> ssh -L 56789:12345 user@remote-server # запускаем gdbserver на порту 12345 remote-server> gdbserver localhost:12345 ./my_program ``` ``` # запускаем интерфейс gdb на локальном компьютере local-pc> gdb # подключаемся к локальному порту 56789 (gdb) target remote localhost:56789 ``` ### Автоматизация рутинных действий .gdbinit Часто требуется запускать отладчик `gdb` многократно, например после внесения каких-либо изменений. Для того, чтобы не вводить команды каждый раз при запуске, можно положить рядом с программой текстовый файл `.gdbinit`, который будет выполнять команды при запуске отладчика: ``` # файл .gdbinit рядом с программой # загружаем файл с программой file my_program # ставим точку останова в функцию main break main # переключаем интерфейс в отображение исходников layout src # подключаемся к удаленному серверу :12345 target remote localhost:12345 # запускаем программу до первой точки останова continue ``` По соображениям безопасности, `gdb` не будет выполнять произвольные скрипты, которые лежат где угодно. Для этого необходимо указать отладчику каталог, который считается надежным, и тем самым сделать исключение. Для этого нужно дописать строчки в файл `~/.gdbinit`: ``` # разрешить загрузку локальных скриптов .gdbinit # из каталога ~/i-love-akos/ и всех его подкаталогов set auto-load safe-path ~/i-love-akos/ ``` ================================================ FILE: practice/linux_basics/cmake.md ================================================ # Система сборки `cmake` Использование сторонних библиотек усложняет процесс воспроизводимости сборки. В случае, когда целевая операционная система одна, это не доставляет особых проблем и достаточно простого `Makefile`, но если предполагается разработка кросс-платформенного продукта, то возникают неоднозначности: * какой компилятор используется для сборки (gcc, clang, cl.exe); * расположение include-файлов библиотек для компиляции; * рссположение файлов библиотек для компоновки. По этой причине часто практикуется не распространение `Makefile`, написанного для конкретного стека инструментов, а его генерация по декларативному описанию в процессе сборки: `./configure`, `qmake`, или `cmake`. Наиболее гибкой системой сборки, и при этом относительно простой, является [CMake](https://cmake.org/cmake/help/v3.2/), которая реализована с поддержкой не только UNIX-подобных систем, но и Windows. ## Проект CMake и его сборка Описание проекта находится в файле `CMakeLists.txt` и имеет примерно следующий вид: ```cmake # признак того, что это файл для cmake # номер версии - это минимально требуемый для сборки проекта # не стоит злоупотребять указанием самой свежей версии # CMake, поскольку в консервативных Linux-дистрибутивах # может быть что-то более старое cmake_minimum_required(VERSION 3.2) # имя проекта - не обязательно, обычно используется IDE project(my_great_project) # команда `set` устанавливает значение переменной # некоторые переменные (их имена начинаются с CMAKE_) # имеют специальное значение # дополнительные опции компилятора Си set(CMAKE_C_FLAGS "-std=gnu11") # дополнительные опции компилятора C++ set(CMAKE_CXX_FLAGS "-std=gnu14") # в переменной SOURCES будет храниться список файлов; # если файлов не много, то можно этого не делать, # но некоторым IDE это требуется для навигации по проекту set(SOURCES file1.c file2.cpp file3.cpp ) # добавление цели для сборки - бинарного файла; # синтаксис ${...} означает использование значения # переменной, которая в данном примере будет раскрыта # в список файлов, из которых собирается программа add_executable(my_cool_program ${SOURCES}) ``` Для сборки CMake-проекта необходимо выполнить две стадии: 1. Сгенерировать `Makefile`из `CMakeLists.txt` 2. Собрать проект обычнычным инструментом `make`. Обычно в процессе генерации `Makefile` и при сборке проекта создается много временных файлов. По этой причине сборку принято проводить в отдельном каталоге, - чтобы не засорять каталог с исходными текстами. ```bash $ mkdir build # создаем каталог для сборки $ cd build # переходим в него $ cmake ../ # генерируем Makefile # аргумент cmake - это каталог, который # содержит файл CMakeLists.txt $ make # запуск компиляции ``` ## Использование сторонних библиотек, для который есть готовое описание CMake Для многих OpenSource библиотек в стандартной поставке CMake уже готовы модули поддержки, которые выполняют поиск библиотеки. В случае c UNIX этот поиск осуществляется с помощью запуска команд конфигурации, либо проверки различных вариантов написания имен файлов в `/usr/include` и `/usr/lib`. Для Windows просматривается системный реестр. Список поддерживаемых библиотек можно найти в поставке CMake, для Linux это может быть каталог (в разных дистрибутивах они разные) `/usr/share/cmake/Modules`. Все файлы модулей имеют название `FindИМЯБИБЛИОТЕКИ.cmake`. Подключение библиотеки, которая поддерживается "из коробки", осуществляется с помощью команды `find_package`. В случае, если необходимые файлы присутствуют, то определяются переменные: * `ИМЯБИБЛИОТЕКИ_FOUND` - переменная, значение которой устанавливается в `1`, если библиотека не отмечена как `REQUIRED`; * `ИМЯБИБЛИОТЕКИ_INCLUDE_DIRS` - список дополнительных каталогов, в которых нужно искать заголовочные файлы (опции компилятора `-I...`); * `ИМЯБИБЛИОТЕКИ_LIBRARIES` - список дополнительных библиотек и каталоги к ним (опции компилятора `-l...` и `-L...`); Пример для curl: ```cmake # найти библиотеку CURL; опция REQUIRED означает, # что библиотека является обязательной для сборки проекта, # и если необходимые файлы не будут найдены, cmake # завершит работу с ошибкой find_package(CURL REQUIRED) add_executable(my_cool_program ${SOURCES}) # добавляет в список каталогов для цели my_cool_program, # которые превратятся в опции -I компилятора для всех # каталоги, которые перечислены в переменной CURL_INCLUDE_DIRECTORIES target_include_directories(my_cool_program ${CURL_INCLUDE_DIRECTORIES}) # для цели my_cool_program указываем библиотеки, с которыми # программа будет слинкована target_link_libraries(my_cool_program ${CURL_LIBRARIES}) ``` Если необходимо использовать библиотеку для всех целей проекта, а не для отдельных, то можно использовать команды `include_directories` и `link_libraries`. ## Использование сторонних библиотек, для которых есть описания `pkg-config` Для многих GNU-библиотек существуют описания их использования, которые можно использовать утилитой [`pkg-config(1)`](https://linux.die.net/man/1/pkg-config). Эти описания можно использовать в проектах CMake в том случае, если для них не реализованы описания CMake, но существуют описания pkg-config. Обычно эти файлы `*.pc` располагаются в каталоге `/usr/lib[64]/pkgconfig`. Пример для fuse3: ```cmake # подключаем модуль интеграции с pkg-config find_package(PkgConfig REQUIRED) pkg_check_modules( FUSE # имя префикса для названий выходных переменных REQUIRED # если библиотека является обязательной fuse3 # имя библиотеки, должен существовать файл fuse3.pc ) # можно использовать переменные FUSE_INCLUDE_DIRECTORIES # и FUSE_LIBRARIES target_include_directories(my_cool_program ${FUSE_INCLUDE_DIRECTORIES}) target_link_libraries(my_cool_program ${FUSE_LIBRARIES}) # дополнительные флаги компиляции, например определения -D..., # которые не указывают на каталоги для поиска заголовочных файлов, # перечислены в переменной FUSE_CFLAGS_OTHER target_compile_options(my_cool_program ${FUSE_CFLAGS_OTHER}) ``` ## Использование сторонних библиотек, для которых вообще ничего нет В случае, если для библиотеки не подготовлено никаких описаний, то можно попытаться найти необходимые файлы, используя перебор различных комбинаций имен и стандартных каталогов: ```cmake # поиск файла динамической библиотеки find_library( SOME_LIBRARY # переменная, в которую будет записан результат NAMES # перечисляются различные варианты имен для поиска some something somelib0 ) # поиск пути к заголовочным файлам find_path( SOME_INCLUDE_DIRECTORY # переменная, в которую будет записан результат NAMES # имена файлов, которые могут содержаться в каталоге somelib.h somelib_common.h PATH_SUFFIXES # возможные имена подкаталогов в .../include/ somelib somelib-1.0 ) target_include_directories(my_cool_program ${SOME_INCLUDE_DIRECTORY}) target_link_libraries(my_cool_program ${SOME_LIBRARY}) ``` ================================================ FILE: practice/linux_basics/devtools.md ================================================ # Инструменты разработчика ## Компиляторы `gcc` и `clang` В стандартную поставку современных UNIX-систем входит один из компиляторов: либо `gcc`, либо `clang`. В случае с Linux, по умолчанию обычно используется `gcc`, а в BSD-системах - `clang`. Далее будет описана работа с компилятором `gcc`, имея ввиду, что работа с `clang` ничем принципиально не отличается: у обоих компиляторов много общего, в том числе опции командной строки. Кроме того, существует команда `cc`, которая является символической ссылкой на используемый по умолчанию компилятор языка Си (`gcc` или `clang`), и команда `c++`, - символическая ссылка на используемый по умолчанию компилятор для C++. Рассмотрим простейшую программу на языке C++: ``` // файл hello.cpp #include int main() { std::cout << "Hello, World!" << std::endl; return 0; } ``` Скомпилировать эту программу можно с помощью команды: ``` > c++ -o program.jpg hello.cpp ``` Опция компилятора `-o ИМЯ_ФАЙЛА` указывает имя выходного файла, который нужно создать. По умолчанию используется имя `a.out`. Обратите внимание, что файл `program.jpg` является обычным выполняемым файлом! ### Стадии сборки программы на Си или C++ При выполнении команды `c++ -o program.jpg hello.cpp` выполняется достаточно сложная цепочка действий: 1. Выполняется *препроцессинг* текстового файла `hello.cpp`. На этом этапе обрабатываются *директивы препроцессора* (которые начинаются с символа `#`), и получается новый текст программы. Если запустить компилятор с опцией `-E`, то будет выполнен только этот шаг, и на стандартный поток вывода будет выведен преобразованный текст программы. 2. Выполняется *трансляция* одного или нескольких текстов на Си или C++ в объектные модули, содержащие машинный код. Если указать опцию `-c`, то на этом сборка программы будет приостановлена, и будут созданы объектные файлы с суффиксом `.o`. Объектные файлы содержат *бинарный* исполняемый код, которому в точности соотвествует некоторый текст на языке ассемблера. Этот текст можно получить с помощью опции `-S`, - в этом случае, вместо объектных файлов будут созданы текстовые файлы с суффиксом `.s`. 3. Компоновка одного или нескольких объектных файлов в исполняемый файл, и связываение его со стандартной библиотекой Си/С++ (ну и другими библиотеками, если требуется). Для выполнения компоновки компилятор вызывает стороннюю программу `ld`. ### Программы на Си v.s. программы на C++ Компилятор `gcc` имеет опцию `-x ЯЗЫК`, для указания языка исходного текста программы: Си (`c`), C++ (`c++`) или Фортран (`fortran`). По умолчанию, язык исходного текста определяется в соответствии с именем файла: `.c`, - это программы на языке Си, а файлы, оканчивающиеся на `.cc`, `.cpp` или `.cxx`, - это тексты на языке C++. Таким образом, имя файла является существенным. Это относится к стадиям препроцессинга и трансляция, но может вызвать проблемы на стадии компоновки. Например, используя команду `gcc` вместо `g++` (или `cc` вместо `c++`), можно успешно скомпилировать исходный текст программы на C++, но при этом возникнут ошибки на стадии связывания, поскольку компоновщику `ld` будут переданы опции, подразумевающие связывание только со стандартной библиотекой Си, но не C++. Поэтому, при сборке программ на C++ нужно использовать команду `c++` или `g++`. ### Указание стандартов Опция компилятора `-std=ИМЯ` позволяет явным образом указать используемый стандарт языка. Рекомендуется явным образом указывать используемый стандарт, поскольку поведение по умолчанию зависит от используемого дистрибутива Linux. Допустимые имена: * `c89`, `c99`, `c11`, `gnu99`, `gnu11` для языка Си; * `c++03`, `c++11`, `c++14`, `c++17`, `gnu++11`, `gnu++14`, `gnu++17` для языка C++. Двузначное число в имени стандарта указывает на его год. Если в имени стандарта присутствует `gnu`, то подразумеваются GNU-расширения компилятора, специфичные для UNIX-подобных систем, и кроме того, считается определенным макрос `#define _DEFAULT_SOURCE`, который в некоторых случаях меняет поведение отдельных функций стандартной библиотеки. В дальнейшем мы будем ориентироваться на стандарт `c11`, а в некоторых задачах, где будет про это явно указано - его расширением `gnu11`. ## Объектные файлы, библиотеки и исполняемые файлы ### Модуль `ctypes` интерпретатора Python Рассмотрим [программу](my-first-program.c) на языке Си: ``` /* my-first-program.c */ #include static void do_something() { printf("Hello, World!\n"); } extern void do_something_else(int value) { printf("Number is %d\n", value); } int main() { do_something(); } ``` Скомпилируем эту программу в объектный файл, а затем - получим из него: (1) выполняемую программу; (2) разделяемую библиотеку. Обратите внимание на опцию `-fPIC`, предназначенную для генерации позиционно-независимого кода, о чем будет рассказано на одном из последующих семинарах. ``` > gcc -c -fPIC my-first-program.c > gcc -o program.jpg my-first-program.o > gcc -shared -o library.so my-first-program.o ``` В результате мы получим программу `program.jpg`, которая выводит на экран строку `Hello, World!`, и *библиотеку* с именем `library.so`, которую можно использовать как из Си/C++ программы, так и динамически подгрузить для использования интерпретатором Python: ``` > python3 Python 3.6.5 (default, Mar 31 2018, 19:45:04) [GCC] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from ctypes import cdll >>> lib = cdll.LoadLibrary("./library.so") >>> lib.do_something_else(123) >>> retval = lib.do_something_else(123) Number is 123 >>> print(retval) 14 ``` Обратите внимание, что результатом работы функции `do_something_else` является какое-то загадочное число `14` (возможно, будет какое-то другое при попытке воспроизвети этот эксперимент), хотя функция возвращает `void`. Причина заключается в том, что разделяемые библиотеки хранят только **имена** функций, но не их сигнатуры (типы параметров и возвращаемого значения). Попытка вызвать функцию `do_something` не увенчается успехом: ``` >>> lib.do_something() Traceback (most recent call last): File "", line 1, in File "/usr/lib64/python3.6/ctypes/__init__.py", line 361, in __getattr__ func = self.__getitem__(name) File "/usr/lib64/python3.6/ctypes/__init__.py", line 366, in __getitem__ func = self._FuncPtr((name_or_ordinal, self)) AttributeError: ./library.so: undefined symbol: do_something ``` В этом случае имя `do_something` не найдено, поскольку в исходном тексте на языке Си модификатор `static` перед именем функции явно запрещает использование функции где-либо вне текущего исходного текста. ### Просмотр таблицы символов Для исследования объектных файлов, в том числе и скомпонованных, используется утилита `objdump`. Опция `--syms` или `-t` отображает отдельные секции исполняемого объектного файла, которым присвоены имена - *символы*. Некоторые имена имеют пометку `*UND*`, - это означает, что имя используется в объектном файле, но его расположение неизвестно. Задача компоновщика состоит как раз в том, чтобы найти требуемые имена в разных объектных файлах или динамических библиотеках, а затем - подставить правильный адрес. Некоторые символы помечены как глобальные (символ `g` во втором столбце вывода), а некоторые - как локальные (символ `l`). Те символы, которые не являются глобальными, считаются *не экспортируемыми*, то есть (теоретически) не должны быть доступны извне. ## Отладчик Если скомпилировать программу с опцией `-g`, то размер программы увеличится, поскольку в ней появляются дополнительные секции, которые содержат *отладочную информацию*. Отладочная информация содержит сведения о соответствии отдельных фрагментов программы исходным текстам, и включает в себя номера строк, имена исходных файлов, имена типов, функций и переменных. Эти информация используется только отладчиком, и почти никак не влияет на поведение программы. Таким образом, отладочную информацию можно совмещать с оптимизацией, в том числе достаточно агрессивной (опция компилятора `-O3`). Для запуска программы под отладчиком используется команда `gdb`, в качестве аргумента к которой указывается имя выполняемого файла или команды. Основные команды `gdb`: * `run` - запуск программы, после `run` можно указать аргументы; * `break` - установить точку останова, параметрами этой команды может имя функции или пара `ИМЯ_ФАЙЛА:НОМЕР_СТРОКИ`; * `ni`, `si` - шаг через строку или шаг внутрь функции соответственно; * `return` - выйти из текущей функции наверх; * `continue` - продолжить выполнение до следующей точки останова или исключительной ситуации; * `print` - вывести значение переменной из текущего контекста выполнения. Взаимодействие с отладчиком производится в режиме командной строки. Различные интегрированные среды разработки (CLion, CodeBlocks, QtCreator) являются всего лишь графической оболочкой, использующей именно этот отладчик, и визуализируя взаимодействие с ним. Более подробный список команд можно посмотреть в [CheatSheet](https://www.cheatography.com/fristle/cheat-sheets/closed-source-debugging-with-gdb/). ================================================ FILE: practice/linux_basics/intro.md ================================================ # Введение в ОС Linux ## Это не Windows. Забудьте то, что вы знали раньше ### Используемая терминология * Файловая система представляет собой иерархию файлов и *каталогов*. Не нужно называть каталоги "папками". * В отличии от Windows, все файлы в UNIX являются равнозначными, независимо от их имени. Понятия "расширение файла" не существует, но для удобства восприятия имени файла пользователем, их снабжают *суффиксами имени*, отделяемые от основного имени точкой. Суффиксов у имени файла может быть несколько, например `.tar.gz`. * В системе выполняется огромное количество *процессов*, а не "задач". Процессы могут быть запущены как непосредственно пользователем, так и одним из *демонов*, которые запускаются при загрузке системы, и сами по себе являются процессами. ### Принятые обозначения клавиатурных сокращений При работе с консольными UNIX-программами обычно используются следующие, исторически сложившиеся, обозначения клавиатурных сочетаний: * `C-Буква` - одновременное нажатие `Ctrl` и буквенной клавиши. Вниманию пользователей MacOS! `Ctrl` - это именно клавиша `Ctrl`, а не `Command`. * `M-Буква` - одноваременное нажатие `Alt` и буквенной клавиши. Сокращение "M" - от слова "Meta". Такая клавиша была на старых рабочих станциях Sun и SGI. * `C-Буква1 Буква2` - сначала одновременное нажатие `Ctrl` и `Буква1`, затем отпустить клавишу `Ctrl` и нажать `Буква2`. Аналогично для клавиши `Alt`. Сочетание `C-Буква1` называется *префиксом* клавиатурного сочетания, обычно под одинаковыми префиксами группируются клавиатурные сочетания для действий одного характера * `C-Буква1 C-Буква2`. Нажать `Ctrl`, затем нажать и отпустить `Буква1`, нажать и отпустить `Буква2`. После этого можно отпустить клавишу `Ctrl`. * Клавиши `F13`...`F15`. На PC-клавиатуре их нет. Их нажатие обеспечивается клавишей `Shift` и одной функциональных клавиш с номером `F...` меньше, в зависимости от терминала, на 10 или на 12. Например, во многих графических терминалах, `Shift+F5` означает нажатие клавиши `F15`. ## Начало работы Linux, как и любые другие операционные системы семейства UNIX, является **многопользовательской** операционной системой. Для начала работы необходимо знать свое имя пользователя и пароль. В зависимости от целей использования, вход в систему может быть осуществлен различными способами. ### Локальный вход с графическим интерфейсом Этот вариант обычно используется при установке Linux в качестве Desktop'а. Как правило, в большинстве дистрибутивов Linux предусмотрен автоматический вход в систему, если при установке был указан только один пользователь-человек (существует еще и другой вид пользователей -- системные). Если пользователей несколько, то вход в систему мало чем отличается от такового в ОС Windows или Mac. После входа в систему, отображается графическая оболочка (GNOME, Unity или KDE). Командная строка, с которой мы преимущественно будем работать, доступна с помощью приложения "Терминал". ### Локальный вход без графического интерфейса Этот вариант обычно используется при начальной настройке серверов (графический стек является потенциальной "дырой" в безопасности, и обычно не устанавливается), а также при работе со встраиваемыми системами. После загрузки системы или подключения терминала отображается текстовое приглашение с предложением ввести имя пользователя и пароль, а после входа в систему управление передается **командному интерпретатору**. ### Удаленный вход через SSH Для подключения по SSH необходимо использовать команду (для Linux/Mac) ``` ssh ИМЯ_ПОЛЬЗОВАТЕЛЯ@ИМЯ_ХОСТА ``` Для подключения по SSH из Windows существуют специальные программы, например [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html). После подключения нужно ввести пароль для входа в систему. В некоторых случаях вводить пароль не потребуется, например, если настроена авторизация с использованием SSH-ключей. После входа в систему, управление передается командному интерпретатору. ## Основы работы с командной строкой ### Навигация по файловой системе Приглашение командной строки обычно имеет вид, зависящий от состояния: ``` ИМЯ_ПОЛЬЗОВАТЕЛЯ@ИМЯ_ХОСТА:ТЕКУЩИЙ_КАТАЛОГ> ``` Корневой каталог в иерархии файловой системы -- это `/`. После входа в систему, текущим является *домашний каталог* текущего пользователя, -- это каталог, который доступен как для чтения, так и для записи. Домашние каталоги обычных пользователей располагаются в: * `/home/` -- для Linux * `/usr/local/home` -- для FreeBSD * `/Users` -- для MacOS Независимо от используемой операционной системы, имя `~` (символ "тильда", на одной клавише с буквой "Ё"), является синонимом домашнего каталога *текущего пользователя*. Лексемы `.` и `..` означают, соответственно, текущий каталог и каталог на один уровень выше в иерархии. Для навигации по каталогам используется команда `cd`. Примеры: ``` cd .. # Перейти на уровень выше cd ../.. # Перейти на два уровня выше cd ../src # Перейти на уровень выше, затем в подкаталог src cd / # Перейти в корневой каталог cd /usr/lib64 # Перейти в каталог /usr/lib64 cd ~/projects # Перейти в каталог /home/ИМЯРЕК/projects ``` **Замечание**. При вводе имен файлов или каталогов, клавиша `TAB` вызывает функцию автодополнения имени. ### Запуск выполняемых файлов Выполняемый файл -- это любой файл (в том числе и текстовый), обладающий специальным аттрибутом. Запуск выполняемого файла осуществяется двумя способами: * Вводом имени этого файла в том случае, если файл располагается в одном из каталогов, перечисленных в *переменной окружения* `PATH`. Все стандартные программы, входящие в поставку UNIX-системы запускаются таким способом. * Вводом *полного имени* файла. Полное имя может быть как абсолютным (то есть начинаться с символа `/`), так и относительным (начиначется с символа `.`). Таким способом обычно запускаются программы, находящиеся в домашнем каталоге. ### Стандартные программы для управления файлами * `cp` -- копирование файла или каталога (с опцией `-R`) * `mv` -- переименование (перемещение) файла или каталога (с опцией `-R`) * `rm` -- удаление файла или каталога (с опцией `-r`) * `ls` -- вывод содержимого текущего каталога Все эти команды являются обычными программами, которые располагаются в каталоге `/usr/bin`. **Вопрос**. Почему не существует программы с именем `cd`? ### Форматы исполняемых файлов * Бинарный файл начинается с последовательности байт `0x7F 0x45 0x4C 0x46`. Этот формат называется ELF (Executable and Linkable Format). * Произвольный файл, в том числе текстовый, который начинается с текстовой строки вида `#!ИМЯ_ИНТЕРПРЕТАТОРА\n`. В этом случае, система запускает указанный интерпретатор, и передает ему выполняемый файл в качестве аргумента. Пример выполняемого файла: ``` #!/usr/bin/python print("Hello, UNIX!") ``` ### Midnight Commander Для навигации по файловой системы, использование командной строки, -- это не всегда удобно. **Замечание**. При использовании предоставляемого образа ВМ, файловая система также доступна по FTP: [ftp://student@192.168.56.105/](ftp://student@192.168.56.105/). *Midnight Commander* -- это двухпанельный файловый менеджер, доступный почти для всех UNIX-подобных операционных систем (включая MacOS). Запускается командой `mc` и работа с ним аналогична работе с FAR Manager или Total Commander. Исключение составляют некоторые клавиатурные сочетания. Основные операции: * `F3` -- просмотр файла * `F4` -- редактирование файла * `Shift+F4` -- создание и редактирование нового файла * `F5` -- копирование * `F6` -- перемещение * `F7` -- создание каталога * `F8` -- удаление * `F10` -- выход из Midnight Commander * `C-x c` -- редактирование аттрибутов файла * `C-x o` -- редактирование пользователя файла * `C-x s` -- создание символической ссылки на файл **Замечание.** Выход из редактирования или просмотра файла осуществляется нажатием клавиши `Esc` **два раза**. Это связано с тем, что клавиша `Esc` в классических терминалах предназначена для префиксного ввода управляющих символов. ### Иерархия файловой системы В отличии от Windows, где каждому физическому диску или разделу на диске соответствует определенная буква, например `C:\`, дерево файловой системы в UNIX-системах имеет общий корень `/`. Отдельные диски или разделы *монтируются* в подкаталоги основной файловой системы. Файловая система всех дистрибутивов Linux имеет следующую иерархию: * `/bin` -- выполняемые программы, предоставляющие минимально необходимый набор команд * `/boot` -- файлы, необходимые для загрузки операционной системы * `/dev` -- псевдо-файлы устройств * `/etc` -- текстовые файлы настроек * `/home` -- домашние каталоги пользователей * `/lib` или `/lib64`, либо оба этих каталога -- минимально необходимый для работоспособности системы набор разделяемых библиотек. Каталог `/lib64` присутствует на 64-битных системах и содержит варианты библиотек для x86_64, в то время как `/lib` -- их аналоги для i386. * `/lost+found` -- файлы, которые по каким-либо причинам (например, неправильное выключение компьютера, или сбой диска) оказались вне какого-либо каталога, но их содержимое доступно * `/media` -- каталог для монтирования сменных носителей, доступных всем пользователям * `/mnt` -- каталог для монтирования общедоступных сетевых файловых систем или инородных разделов * `/opt` -- каталог для установки сторонних приложений не из репозитория дистрибутива, например Google Chrome или Яндекс.Браузер * `/proc` -- здесь примонтирована виртуальная файловая система с информацией о запущенных в системе процессах * `/root` -- домашний каталог пользователя `root` * `/run` -- содержит *именованные сокеты* и текстовые файлы с *индентификаторами процесса* для запущенных демонов * `/sbin` -- выполняемые файлы для запуска пользователем `root`; у других пользователей этот каталог не включен в переменную окружения `PATH`, и для их запуска необходимо указывать полный путь * `/srv` -- файлы для хранения данных сетевыми службами * `/sys` -- виртуальная файловая система для просмотра и изменения параметров ядра * `/tmp` -- каталог для временных файлов * `/usr` -- содержит иерархию, аналогичную корневой; там находятся файлы большинства программ, которые устанавливаются из *репозиториев* дистрибутива * `/usr/local` -- аналогично `/usr`, но предназначен для установки программ самостоятельно из исходных текстов * `/var` -- содержит данные различных демонов, например базы данных. ## Консольные текстовые редакторы ### Встроенный редактор Midnight Commander Вызывается нажатием клавиши `F4` из файлового менеджера, либо командой `mcedit ИМЯ_ФАЙЛА` как самостоятельная программа. Основные клавиши: * `F2` -- сохранить файл * `Esc Esc` -- выход * `F3` -- начало/конец выделения текста * `F5` -- копирование выделенного текста в текущую позицию * `F6` -- перемещение выделенного текста в текущую позицию * `F8` -- удаление выделенного текста; если текст не выделен, -- удаление текущей строки ### Редактор VI Поскольку Midnight Commander, и соответственно, редактор `mcedit` не всегда установлены по умолчанию, иногда возникает необходимость использования редактора `vi`, который входит в базовый состав почти всех дистрибутивов Linux. Запускается редактор командой `vi ИМЯ_ФАЙЛА`, либо в результате какого-нибудь действия, которое требует редактирования текста (например, командой `git commit` -- для редактирования комментария к коммиту). Опознать редактор `vi` можно по черному экрану, в левом столбце терминала при этом во всех пустых строках в конце текста присутствует символ `~`. После запуска, редактор находится в **командном режиме**. Не нужно нажимать буквенно-цифровые клавиши для ввода текста в этом режиме. Если это все-же произошло, нужно нажать `C-[` для возврата в командный режим. В командном режиме навигация по тексту осуществляется клавишами стрелок, а также клавишами `h`, `j`, `k`, и `l`. Помимо текстового редактора `vi`, многие среды разработки, например QtCreator и IntelliJ IDEA, а также браузеры имеют Chrome и Firefox имеют плагины, позволяющие использовать навигацию в стиле VIM, так что назначение этих клавиш лучше запомнить. Для перехода в **режим вставки**, привычному по GUI-редакторам, нужно нажать клавишу `i`. Выход из этого режима -- сочетанием `C-[`. Для перехода в **режим замены** -- клавиша `o`, выход аналогичен. Основные команды, которые нужно запомнить: * `:w` -- сохранить файл * `:e ИМЯ_ФАЙЛА` -- открыть или создать файл с указанным именем * `:q` -- выход из редактора, возможен только если нет не сохраненных изменений * `:q!` -- принудительный выход из редактора без сохранения * `!КОМАНДА` -- запуск UNIX-команды без выхода из редактора Более подробное руководство по `vi` можно получить, запустив программу `vimtutor` ### Редактор nano В некоторых дистрибутивах, например Ubuntu, по умолчанию вместо `vi` установлен редактор `nano`. Это простой в использовании текстовый редактор. Опознать его можно по тексту `GNU nano` в заголовке вверху терминала, и подсказкам о сочетаниях клавиш вида `^G Get Help` в подвале. Символ `^` означает клавишу `Ctrl`. ================================================ FILE: practice/linux_basics/my-first-program.c ================================================ /* my-first-program.c */ #include static void do_something() { printf("Hello, World!\n"); } extern void do_something_else(int value) { printf("Number is %d\n", value); } int main() { do_something(); } ================================================ FILE: practice/math/README.md ================================================ # Целочисленная и вещественная арифметика ## Целочисленные типы данных Минимально адресуемым размером данных является, какправило, один байт (8 бит). Как правило - это значит, что не всегда, и бывают разные экзотические архитектуры, где "байт" - это 9 бит (PDP-10), или специализированные сигнальные процессоры с минимально адресуемым размером данных 16 бит (TMS32F28xx). По стандарту языка Си определена константа `CHAR_BIT` (в заголовочном файле ``), для которой гарантируется, что `CHAR_BIT >= 8`. Тип данных, представляющий один байт, исторически называется "символ" - `char`, который содержит ровно `CHAR_BIT` количество бит. Знаковость типа `char` по стандарту не определена. Для архитектуры x86 это знаковый тип данных, а, например, для ARM - беззнаковый. Опции компилятора gcc `-fsigned-char` и `-funsigned-char` определяют это поведение. Для остальных целочисленных типов данных: `short`, `int`, `long`, `long long`, стандарт языка Си определяет минимальную разрядность: | Тип данных | Разрядность | | -----------| -------------------------------| | `short` | не менее 16 бит | | `int` | не менее 16 бит, обычно 32 бит | | `long` | не менее 32 бит | | `long long`| не менее 64 бит, обычно 64 бит | Таким образом, полагаться на количество разрядов в базовых типах данных нельзя, и это нужно проверять с помощью оператора `sizeof`, который возвращает "количество байт", то есть, в большинстве случает - сколько блоков размером `CHAR_BIT` помещается в типе данных. С особой осторожностью нужно относиться к типу данных `long`: на 64-разрядной системе Unix он является 64-битным, а, например, на 64-битной Windows - 32-битным. Поэтому, во избежание путаницы, использовать этот тип данных запрещено. ## Знаковые и беззнаковые типы данных Перед целочисленными типами данных могут стоять модификаторы `unsigned` или `signed`, которые указывают допустимость отрицательных чисел. Для знаковых типов, старший бит определяет знак числа: значение `1` является признаком отрицательности. Способ внутреннего представления отрицательных чисел не регламентирован [стандартом языка Си](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570), однако все современные компьютеры используют обратный дополнительный код. Более того, п.6.3.1.3.2 стандарта языка Си определяет способ приведения типов от знакового к беззнаковому таким способом, которые приводит к кодированию обратным дополнительным кодом. Таким образом, значение `-1` представляется как целое число, все биты которого равны единице. С точки зрения низкоуровневого программирования, и языка Си в частности, знаковость типов данных определяет только способ применения различных операций. ## Типы данных с фиксированным количеством бит В заколовочных файлах файле `` (для Си99+) и `` (для C++11 и новее) определены типы данных, для которых гарантируется фиксированное количесвто разрядов: `int8_t`, `int16_t`, `int32_t`, `int64_t`, - для знаковых, и `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` - для беззнаковых. # Переполнение Ситуация целочисленного переполнения возникает, когда тип данных результата не имеет достаточно разрядов для того, чтобы хранить итоговый результат. Например, при сложении беззнаковых 8-разрядных целых чисел 255 и 1, получается результат, который не может быть представим 8-разрядным значением. Для **беззнаковых чисел** ситуация переполнения является штатной, и эквивалентна операции "сложение по модулю". Для **знаковых** типов данных - приводит к ситуации *неопределенного поведения* (Undefined Behaviour). В корректных программах такие ситуации встречаться не могут. Пример: ``` int some_func(int x) { return x+1 > x; } ``` С точки зрения здравого смысла, такая программа должна всегда возвращать значение `1` (или `true`), поскольку мы знаем, что `x+1` всегда больше, чем `x`. Компилятор может использовать этот факт для оптимизации кода, и всегда возвращать истинное значение. Таким образом, поведение программы зависит от того, какие опции оптимизации были использованы. ## Контроль неопределенного поведения Свежие версии компиляторов `clang` и `gcc` (начиная с 6-й версии) умеют контролировать ситуации неопределенного поведения. Можно включить генерацию *управляемого* кода программы, который использует дополнительные проверки во время выполнения. Естественно, это происходит ценой некоторого снижения производительности. Такие инструменты называются *ревизорами* (sanitizers), предназначенными для разных целей. Для включения ревизора, контролирующего ситуацию неопределенного поведения, используется опция `-fsanitize=undefined`. ## Контроль переполнения, независимо от знаковости Целочисленное переполнение означает перенос старшего разряда, и многие процессоры, включая семейство x86, позволяют это диагностировать. Стандартами языков Си и C++ эта возможность не предусмотрена, однако компилятор gcc (начиная с 5-й версии) предоставляет **нестандартные** встроенные функции для выполнения операций с контролем переполнения. ``` // Операция сложения bool __builtin_sadd_overflow (int a, int b, int *res); bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res); bool __builtin_uadd_overflow (unsigned int a, unsigned int b, unsigned int *res); bool __builtin_uaddl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res); bool __builtin_uaddll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res); // Операция вычитания bool __builtin_ssub_overflow (int a, int b, int *res) bool __builtin_ssubl_overflow (long int a, long int b, long int *res) bool __builtin_ssubll_overflow (long long int a, long long int b, long long int *res) bool __builtin_usub_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_usubl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_usubll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) // Операция умножения bool __builtin_smul_overflow (int a, int b, int *res) bool __builtin_smull_overflow (long int a, long int b, long int *res) bool __builtin_smulll_overflow (long long int a, long long int b, long long int *res) bool __builtin_umul_overflow (unsigned int a, unsigned int b, unsigned int *res) bool __builtin_umull_overflow (unsigned long int a, unsigned long int b, unsigned long int *res) bool __builtin_umulll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res) ``` ## Числа с плавающей точкой в формате IEE754 Два основных типа вещественных с плавающей точкой, которые определены стандартом языка Си, - это `float` (используется 4 байта для хранения) и `double` (используется 8 байт). Самый старший бит в представлении числа - это признак отрицательного значения. Далее, по старшинству бит, хранится значения *смещенной экспоненциальной* части (8 бит для `float` или 11 бит для `double`), а затем - значение *мантиссы* (23 или 52 бит). Смещение экспоненциальной части необходимо для того, чтобы можно было в таком представлении хранить значения с отрицательной экспонентой. Смещение для типа `float` равно `127`, для типа `double` - `1023`. Таким образом, итоговое значение может быть получено как: ``` Value = (-1)^S * 2^(E-B) * ( 1 + M / (2^M_bits - 1) ) ``` где `S` - бит знака, `E` - значение смещенной экспоненты, `B` - смещение (127 или 1023), а `M` - значение мантиссы, `M_bits` - количество бит в экспоненте. ## Как получить отдельные биты вещественного числа Поразрядные операции относятся к целочисленной арифметике, и не предусмотрены для типов `float` и `double`. Таким образом, нужно сохранить вещественное число в памяти, и затем прочитать его, интерпретируя как целое число. В случае с языком C++ для этого предназначен оператор `reinterpret_cast`. Для языка Си есть два способа: использовать аналог `reinterpret_cast` - приведение указателей, либо использовать тип `union`. ### Приведение указателей ``` // У нас есть некоторое целое вещественное число, которое хранится в памяти double a = 3.14159; // Получаем указатель на это число double* a_ptr_as_double = &a; // Теряем информацию о типе, приведением его к типу void* void* a_ptr_as_void = a_ptr_as_void; // Указатель void* в языке Си можно присваивать любому указателю uint64_t* a_ptr_as_uint = a_ptr_as_void; // Ну а дальше просто разыменовываем указатель uint64_t b = *a_as_uint; ``` ### Использование типа `union` Тип `union` - это тип данных, который синтаксически очень похож на тип `struct`, то есть там можно перечислить несколько именованных полей, но концептуально - это совершенно разные типы данных! Если в структуре или классе, для хранения каждого поля для предусмотрено отдельное место в памяти, то для `union` этого не происходит, и все поля накладываются друг на друга при размещении в памяти. Обычно тип `union` используется в качестве вариантного типа данных (в С++ начиная с 17-го стандарта для этого предусмотрен `std::variant`), но в качестве побочного эффекта - его удобно использовать приведения типов в стиле `reinterpret_cast`, не используя при этом указатели. ``` // У нас есть некоторое целое вещественное число, которое хранится в памяти double a = 3.14159; // Используем тип union typedef union { double real_value; uint64_t uint_value; } real_or_uint; real_or_uint u; u.real_value = a; uint64_t b = u.uint_value; ``` ## Специальные значения в формате IEEE754 * Бесконечность: `E=0xFF...FF`, `M=0` * Минус ноль (результат деления 1 на минус бесконечность): `S=1`, `E=0`, `M=0` * NaN (Not-a-Number): `S` - любое, `E=0xFF...FF`, `M <> 0` Некоторые процессоры, например архитектуры x86, поддерживают расширение стандарта, позволяющее более эффективно представлять множество чисел, значения которых близко к нулю. Такие числа называются *денормализованными*. Признаком денормализованного числа является значение смещенной экспоненты `E=0`. В этом случае, численное значение получается следующим образом: ``` Value = (-1)^S * ( M / (2^M_bits - 1) ) ``` ### Значения Not-a-Number Некоторые процессоры, например Intel x86, различают два вида чисел `NaN` - невалидное значение. #### sNaN - Signaling NaN Значения `sNaN` возникают при выполнении операций, которые сигнализируют об ошибке на уровне прерывания процессора. Например, деление на `0`. Обычно, чтобы получить такие значения, необходимо собирать программу с опцией `-fno-signaling-nans`. Более подробно - см. [FloatingPointMath - GCC Wiki](https://gcc.gnu.org/wiki/FloatingPointMath) Пример битовой маски для типа `double` на x86_64, определяющей значение `sNaN`: ``` Номера битов 6 5 4 3 2 1 0 3210987654321098765432109876543210987654321098765432109876543210 ---------------------------------------------------------------- Значения 0111111111110100000000000000000000000000000000000000000000000000 ---------------------------------------------------------------- Регион SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM ``` #### qNaN - Quiet NaN В отличии от `sNaN`, значения Quiet NaN, которые получаются в результате вычислений, не приводят к прерыванию процессора и вызове обработчика исплючительной ситуации. Примером является попытка сложить `+inf` и `-inf`. Пример битовой маски для типа `double` на x86_64, определяющей значение `qNaN`: ``` Номера битов 6 5 4 3 2 1 0 3210987654321098765432109876543210987654321098765432109876543210 ---------------------------------------------------------------- Значения 0111111111111000000000000000000000000000000000000000000000000000 ---------------------------------------------------------------- Регион SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM ``` ================================================ FILE: practice/mmap/README.md ================================================ # Страницы памяти в виртуальном адресном пространстве ## Инструменты Кроме пошагового отладчика `gdb`, при работе с памятью существуют дополнительные инструменты, предназначенные для выявления проблем. ### Интерпретируемое выполнение с помощью `valgrind` Набор инструментов `valgrind` использует контролируемое выполнение инструкций программы, модифицируя её код перед выполнением на физическом процессоре. Основные инструменты: * `memcheck` - диагностика проблем с памятью: неверные указатели на кучу, повторное освобождение, чтение неинициализированных данных и забытое освобождение памяти. * `callgrind` - диагностика производительности выполняемой программы. Для запуска программы под valgrind необходимо собрать программу с отладочной информацией (опция компиляции `-g`), в противном случае вывод valgrind будет не информативным. Запуск: ``` > valgrind --tool=ИНСТРУМЕНТ program.jpg ARG1 ARG2 ... ARGn ``` В случае использования инструмента `callgrind`, после выполнения программы генерируется файл `callgrind.out` в формате XML, который можно визуализировать с помощью KCacheGrind (из KDE во всех современных дистрибутивах Linux), либо его кросс-платформенном аналоге [QCacheGrind](https://sourceforge.net/projects/qcachegrindwin/). ### Runtime-проверки ошибок с помощью sanitizer'ов Требует для своей работы свежие версии `clang` или `gcc`, и позволяют выполнять инструментальный контроль во время выполнения программы значительно быстрее, чем `valgrind`. Реализуются на уровне генерации кода и замены некоторых функций, например `malloc`/`free` на реализации с дополнительными проверками. Основные санитайзеры: * AddressSanitizer (`-fsanitize=address`) - диагностирует ситуации утечек памяти, двойного освобождения памяти, выхода за границу стека или кучи, и использования указателей на стек после завершения работы функции. * MemorySanitizer (`-fsanitize=memory`) - диагностика ситуаций чтения неинициализированных данных. Требует, чтобы программа, и как и все используемые ею библиотеки, были скомпилированы в позиционно-независимый код. * UndefinedBehaviourSanitizer (`-fsanitize=undefined`) - диагностика неопределенного поведения в целочисленной арифметике: битовые сдвиги, знаковое переполнение, и т.д. ## Системный вызов mmap ``` #include void *mmap( void *addr, /* рекомендуемый адрес отображения */ size_t length, /* размер отображения */ int prot, /* аттрибуты доступа */ int flags, /* флаги совместного отображения */ int fd, /* файловый декскриптор фала */ off_t offset /* смещение относительно начала файла */ ); int munmap(void *addr, size_t length) /* освободить отображение */ ``` Системный вызов `mmap` предназначен для создания в виртуальном адресном пространстве процесса доступной области по определенному адресу. Эта область может быть как связана с определенным файлом (ранее открытым), так и располагаться в оперативной памяти. Второй способ использования обычно реализуется в функциях `malloc`/`calloc`. Память можно выделять только постранично. Для большинства архитектур размер одной страницы равен 4Кб, хотя процессоры архитектуры x86_64 поддерживают страницы большего размера: 2Мб и 1Гб. В общем случае, никогда нельзя полагаться на то, что размер страницы равен 4096 байт. Его можно узнать с помощью команды `getconf` или функции `sysconf`: ``` # Bash > getconf PAGE_SIZE 4096 /* Си */ #include long page_size = sysconf(_SC_PAGE_SIZE); ``` Параметр `offset` (если используется файл) обязан быть кратным размеру страницы; параметр `length` - нет, но ядро системы округляет это значение до размера страницы в большую сторону. Параметр `addr` (рекомендуемый адрес) может быть равным `NULL`, - в этом случае ядро само назначает адрес в виртуальном адресном пространстве. При использовании отображения на файл, параметр `length` имеет значение длины отображаемых данных; в случае, если размер файла меньше размера страницы, или отображается его последний небольшой фрагмент, то оставшаяся часть страницы заполняется нулями. Страницы памяти могут флаги аттрибутов доступа: * чтение `PROT_READ`; * запись `PROT_WRITE`; * выполнение `PROT_EXE`; * ничего `PROT_NONE`. В случае использования отображения на файл, он должен быть открыт на чтение или запись в соответствии с требуемыми аттрибутами доступа. Флаги `mmap`: * `MAP_FIXED` - требует, чтобы память была выделена по указаному в первом аргументе адресу; без этого флага ядро может выбрать адрес, наиболее близкий к указанному. * `MAP_ANONYMOUS` - выделить страницы в оперативной памяти, а не связать с файлом. * `MAP_SHARED` - выделить страницы, разделяемые с другими процессами; в случае с отображением на файл - синхронизировать изменения так, чтобы они были доступны другим процессам. * `MAP_PRIVATE` - в противоположность `MAP_SHARED`, не делать отображение доступным другим процессам. В случае отображения на файл, он доступен для чтения, а созданные процессом изменения, в файл не сохраняются. ================================================ FILE: practice/mutex-condvar-atomic/README.md ================================================ # Синхронизация потоков ## Проблема гонки данных На всех современных процессорных архитектурах операции чтения и записи в память выровненных данных, размер которых не превышает машинного слова, являются атомарными. Однако, с точки зрения строгому следованию стандартам Си и C++, такое предположение неверно, и необходимо использовать специальные типы данных и атомарные операции. Кроме того, операции над типами данных, размер которых превышает размер машинного слова, заведомо являются неатомарными. В первую очередь, такая проблема проявляется для 64-разрядных типов данных (`double` и `int64_t`) на 32-разрядных архитектурах. Если несколько потоков или процессов обращаются к одной области памяти, причем один из потоков (процессов) производит запись, то такая ситуация называется *гонкой данных* (data race), и приводит к ситуации неопределенного поведения. Пример: ``` int64_t balance = 0; // Thread-1 void* thread_func_one(void *arg) { balance += (int64_t) arg; } // Thread-2 void* yet_another_thread_func(void *arg) { if (balance > 0) { // use balance value } else { // fail } } ``` В данной программе гонка данных возникает по причине того, что поток `Thread-2` полагается на неизменность значения переменной `balance` во время работы функции. В то же время, поток `Thread-1` совершенно независимо меняет это значение. ## Критические секции Часть программы, которая подразумевает монопольное использование какого-либо набора переменных, называется *критической секцией*. Начало критической секции подразумевает установки блокировки, которая будет препятствовать выполнению остальных потоков до тех пор, пока блокировка не будет снята. Стандартным инструментом для обозначения началала критической секции является захват *мьютекса*. Мьютекс - это примитив синхронизации, который, в большинстве случаев, имеет два состояния. Исключение - рекурсивные мьютексы, которые один поток может захватывать N раз, но остальные потоки не смогут захватить мьютекс до тех пор, пока он не будет N раз освобожден. Мьютекс объявлен в заголовочном файле ``, и функции работы с ним требуют линковки с библиотекой `-pthread`. * `pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)` - инициализация мьютекса для его последующего использования. Если второй параметр `NULL`, то инициализируется обычный (не рекурсивный) мьютекс. Для создания нового инициализированного мьютекса с параметрами по умолчанию можно использовать макрос `PTHREAD_MUTEX_INITIALIZER`. * `pthread_mutex_destroy(pthread_mutex_t *mutex)` - уничтожить ранее созданный мьютекс. * `pthread_mutex_lock(pthread_mutex_t *mutex)` - захватить мьютекс. Если другой поток уже захватил его, то текущий поток приостанавливает свою работу. * `pthread_mutex_trylock(pthread_mutex_t *mutex)` - пытается захватить мьютекс. В случае успеха возвращает значение `0`, а если мьютекс уже занят, то значение `EBUSY`. * `pthread_mutex_unlock(pthread_mutex_t *mutex)` - освободить ранее захваченный мьютекс. В отличии от семафоров, освободить мьютекс может только тот поток, которые его захватил, в противном случае это приведет к ошибке `EPERM`. ## Условные переменные Условная переменная - это некоторый примитив синхронизации, используемый для нотификации одним из потоков о наступлении некоторого события, например, готовности данных. Использование условных переменных позволяет отказаться от необходимости применения операций ввода-вывода, и тем самым, исключают необходимость испоьзования системных вызовов. С условной переменной связан определенный мьютекс, который разблокируется на время ожидания. * `pthread_cond_init(pthread_cond_t *c, const pthread_condattr_t *attr)` - инициализации условной переменной. Второй параметр может быть `NULL` - в этом случае подразумевается использование переменной только в рамках одного процесса. Для инициализации условной переменной с параметрами по умолчанию используется макрос `PTHREAD_COND_INITIALIZER`. * `pthread_cond_destroy(pthread_cond_t *c)` - уничтожить условную переменную. * `pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m)` - ожидает нотификации условной переменной переменной `c`, временно разблокируя мютекс `m`. Перед вызовом мьютекс должен находиться в заблокированном состоянии, в противном случае - неопределенное поведение. После наступления события нотификации, мьютекс опять блокируется. * `pthread_cond_timedwait(pthread_cond_t *c, pthread_mutex_t *m, const struct timespec *timeout)` - то же, что и `pthread_cond_wait`, но ожидание прекращается по истечению указанного периода времени. * `pthread_cond_signal(pthread_cond_t *c)` - уведомляет один поток, для которого выполняется ожидание нотификации. В общем случае, поток выбирается случайным образом, если их несколько. * `pthread_cond_broadcast(pthread_cond_t *c)` - уведомляет все потоки, для которых выполняется ожидание нотификации. В случае, если ни один поток не вызвал `pthread_cond_wait`, то нотификация условной переменной проходит незамеченной. ## Атомарные переменные и неблокирующие структуры данных Необходимость блокировки мьютекса, защищающего критическую секцию, может приводить к простою потоков, которые не собираются ничего модифицировать. В ситуации, когда потоков много, это существенно сказывается на производительности, и становится более выгодным использование lock-free структур данных, например односвязных списков. Основная идея lock-free структур заключается в том, что любая модификация данных проводится каким-либо потоком в области памяти, которая не разделяется с другими потоками. В тот момент времени, когда данные подгтовлены и должны стать доступны остальным потокам, указатель на них записывается в достижимую другим потокам переменную, причем это происходит атомарным образом. Атомарность достигается за счёт того, что размер указателя в памяти не превышает размера машинного слова. Начиная со 2011 года, в языке Си появлилось новое глючевое слово `_Atomic` - модификатор типа, регламентирующий атомарный тип данных. В отличии от языка C++, где атомарным может быть произвольный объект (не совсем честно, поскольку для составных типов используется мьютекс), множество допустимых атомарных типов данных для языка Си ограничивается только типами, которые умещаются в машинное слово. В противном случае, ключевое слово `_Atomic` и атомарные операции не имеют никакого значения. Атомарные операции над типами, объявленными как `_Atomic`, реализуются в Си11 как макросы: * `void atomic_store(T* object, T value)`, * `void atomic_store_explicit(T* object, T value, memory_order order)` - сохранить значение в атомарную переменную. * `T atomic_load(T* object)`, * `T atomic_load_explicit(T* object, memory_order order)` - загрузить значение из переменной. * `T atomic_exchange(T* object, T new_value)`, * `T atomic_exchange_explicit(T* object, T new_value, memory_order order)` - заменить значение и вернуть предыдущее. * `_Bool atomic_compare_exchange_strong(T* object, T* expected, T new_value)`, * `_Bool atomic_compare_exchange_strong_explicit(T* object, T* expected, T new_value, memory_order success, memory_order failure)`, * `_Bool atomic_compare_exchange_weak(T* object, T* expected, T new_value)`, * `_Bool atomic_compare_exchange_weak_explicit(T* object, T* expected, T new_value, memory_order success, memory_order failure)` - сравнить два значения, в случае их равенства - заменить на новое, в противном случае - записать в `expected` значение `object`. * `T atomic_fetch_MOD(T* object, T operand)`, * `T atomic_fetch_MOD_explicit(T* object, T operand, memory_order order)` - получить значение переменной, после чего - модифицировать её. `MOD` можеть быть: - `add` - инкремент - `sub` - декремент - `and` - поразрядное "и" - `or` - поразрядное "или" - `xor` - поразрядное "исключающее или". В операции `compare_exchange` вариант `weak` обычно работает быстрее, но может ложно возвращать значение `0` даже при равенстве значений в `object` и `expected`. В некоторых алгоритмах это бывает допустимо, например, если значение проверяется в цикле. ## Модели памяти Компиляторы могут выполнять нетривиальные преобразования программ, применяя различные эвристики о том, как можно увеличить производительность программ. Компилятор имеет полное право выносить некоторые повторяющиеся дейсвтия из тела цикла, хранить значения переменных в регистрах, а не в памяти, и переставлять отдельные операции местами, если это не противоречит семантике исходной *однопоточной* программы. Все это приводит к тому, что фактически наблюдаемый порядок изменения значений переменных в памяти для одного потока может отличаться от того порядка, которые предполагает другой поток. Пример: ``` // Thread-1 y = 0; x = 0; ... y = 2; x = 1; // Thread-2 if (2 == y) { z = x; // 0 или 1? } ``` В данном случае переменная `z` может иметь значения как `0`, так и `1`, поскольку компилятор мог переставить местами инструкции `y=2` и `x=1`, считая такую перестановку не влияющей на выполнение кода функции, выполняемой в потоке `Thread-1`. Атомарные операции обычно окружены какими-то соседними инструкциями над обычными (не атомарными) переменными. *Модель памяти (memory order)* определяет, какие ограничения накладываются на перестановку обычных операций вокруг атомарных. Ограничения `memory_order`: * `memory_order_relaxed` - отсутствие каких-либо ограничений на оптимизацию. * `memory_order_consume` - на существующих современных архитектурах совпадает с `memory_order_relaxed`. * `memory_order_acquire` - запрещает перестановку операций работы с памятью вперед данной операции. * `memory_order_release` - запрещает перестановку операций работы с памятью после данной операции. * `memory_order_acq_rel` - одновременно `memory_order_acquire` и `memory_order_release`. * `memory_order_seq_cst` - самые сильные ограничения на перестановку инструкций; все нити видят операции в том порядке, как они были прописаны в коде программы. По умолчанию (то есть без явного указания модели памяти) подразумевается самая строгая модель `seq_cst`. ================================================ FILE: practice/openssl/README.md ================================================ # Шифрование с использованием OpenSSL/LibreSSL ## Основы шифрования в Linux Криптография в Linux, как и во многих других UNIX-подобных системах реализована с помощью пакета `openssl` или совместимого с ним форка `libressl`. Пакет предоставляет: * команду `openssl` для выполнения операций в командной строке * библиотеку `libcrypto` с реализацией алгоритмов шифрования * библиотеку `libssl` с реализацией взаимодействия по протоколам SSL и TLS. ### Вычисление хеш-значений Команды: * `openssl md5` * `openssl sha256` * `openssl sha512` вычисляют хеш-значение для указанного файла и выводят его в читаемом виде на стандартный поток выввода. Дополнительная опция `-binary` указывает вывод в бинарном формате. Если имя файла не указано, то вычисляется хеш-значение для данных со стандартного потока ввода. ### Симметричное шифрование Команда: ``` openssl enc -ШИФР -in ИМЯФАЙЛА -out ВЫХФАЙЛ ``` Выполяет шифрование *симметричным ключем*, то есть некоторым "паролем", который является одинаковым как для шифрования, так и для обратной операции дешифрования. Полный список поддерживаемых шифров отображается командой `openssl enc -ciphers`. Наиболее часто используемые: * `des` - достаточно старый алгоритм с использованием 56-битного ключа; * `aes256` или `aes-256-cbc` - более надежный и достаточно быстрый; * `base64` - без шифрования (ключ не требуется); удобный способ конвертировать бинарные файлы в текстовое представление и обратно. Опция `-d` означает обратное преобразование, то есть *дешифрование*. Опция `-base64` подразумевает, что зашифрованные данные дополнительно преобразуются в кодировку Base64, например, для передачи данных в виде текста. После запуска команды будет запрошен пароль и его подтверждение. В случае, когда нужно автоматизировать запуск команды, используется опция `-pass`, после которой передается, каким именно образом задается пароль: * `pass:ПАРОЛЬ` - пароль задается обычным текстом в виде аргумента командной строки; жутко небезопасно; * `env:ПЕРЕМЕННАЯ` - пароль задается определенной переменной окружения; немного лучше, но можно выяснить через `/proc/.../environ`; * `file:ИМЯФАЙЛА` - пароль берется из файла; * `fd:ЧИСЛО` - пароль берется из файлового дескриптора с указанным номером; используется при запуске через `fork`+`exec`. Поскольку алгоритмы симметричного шифрования подразумевают использование ключа фиксированного размера, текстовый пароль произвольной длины предварительно преобразуется с помощью хеш-функции. По умолчанию используется SHA-256, но это можно задавать с помощью опции `-md АЛГОРИТМ`. Помимо пароля, в ключ входит ещё одна составляющая - *соль* размером 8 байт, которая хранится в самом зашифрованном файле. Это значение генерируется случайным образом, но для возпроизводимости результата может быть явным образом задана с помощью опции `-S HEX`, где `HEX` - восьмибайтное значение в шестнадцатеричной записи. ### Шифрование с использованием пары ключей Стандартным алгоритмом для шифрования с использованием пары ключей считается RSA. Генерация ключей осуществляется командой: ``` openssl genrsa -out ФАЙЛ РАЗРЯДНОСТЬ ``` Если имя выходного файла не указано, то ключ в текстовом формате будет сохранен на стандартный поток вывода. Обычно ключи RSA хранят в файлах с суффиксом имени `.pem`. Разрядность определяет стойкость ключа, по умолчанию - 2048 бит. Поскольку приватный ключ где-то должен храниться, причем безопасным методом, хорошей практикой считается его хранение в зашифрованном виде, для этого используется шифрование с симметричным ключем: ``` openssl genrsa -aes256 -passout ОПЦИИ_ПАРОЛЯ ``` При использовании зашифрованного закрытого ключа, необходимо будет каждый раз указывать пароль, заданный при его создании. Извлечение публичного ключа из приватного осуществляется командой: ``` openssl rsa -in ПРИВАТНЫЙ_КЛЮЧ -out ПУБЛИЧНЫЙ_КЛЮЧ -pubout ``` Если при создании пары ключей использовалось шифрование, то необходимо ввести пароль, либо задать его через `-passin`. Шифрование с использованием открытого ключа: ``` openssl rsautl -encrypt -pubin -inkey ПУБЛИЧНЫЙ_КЛЮЧ -in ФАЙЛ -out ВЫХОД ``` Обратная операция с использованием закрытого ключа: ``` openssl rsautl -decrypt -inkey ПРИВАТНЫЙ_КЛЮЧ -in ФАЙЛ -out ВЫХОД ``` Ограничением алгоритма RSA является то, что размер шифруемых данных не может превышать размер ключа. С этим можно бороться следующими способами: 1. Делить исходные данные на блоки размером по 2 или 4 Кбайт и шифровать их по-отдельности 2. Генерировать случаным образом одноразовый *сеансовый ключ*, который будет использован в паре с алгоритмом симметричного шифрования, но сам будет зашифрован с помощью RSA. ``` # Генерируем случайный ключ длиной 30 байт и сохраняем # его текстовое Base64 представление в переменной $KEY KEY=`openssl rand -base64 30` # Шифруем симметричный ключ с помощью открытого ключа RSA echo $KEY | openssl rsautl -encrypt -pubin \ -inkey public.pem \ -out symm_key_encrypted.bin # Шифруем данные из большого файла README.md симметричным # ключем из переменной $KEY, которая предварительно # экспортируется, чтобы быть доступной дочернему процессу export KEY openssl enc -aes256 -in README.md \ -out README_encrypted.bin \ -pass env:KEY # Забываем сеансовый ключ - он больше не нужен unset KEY ``` Далее можно смело передавать по незащищенному каналу файлы `README_encrypted.bin`, который содержит данные, и `symm_key_encrypted.bin` с зашифрованным симметричным ключем. Для расшифровки необходимо восстановить сеансовый симметричный ключ, и используя его - дешифровать данные: ``` # Расшифровываем сеансовый симметричный ключ с помощью # приватного ключа RSA и сохраняем в переменной $KEY KEY=`openssl rsautl -decrypt \ -inkey private.pem \ -in symm_key_encrypted.bin` # Выполняем декодирование файла с данными, используя # полученный сеансовый ключ export KEY openssl enc -d -aes256 -pass env:KEY \ -in README_encrypted.bin # Забываем расшифрованный сеансовый ключ unset KEY ``` ## API библиотеки `libcrypto` В качестве онлайн-документации по API OpenSSL удобнее использовать документацию из проекта [LibreSSL](https://www.libressl.org). ### Использование с CMake Если сторонний фреймворк состоит из нескольких библиотек, то команда `find_package` позволяет указать, какие именно необходимо использовать, указав их перечень после `COMPONENTS`: ``` find_package(OpenSSL COMPONENTS Crypto REQUIRED) ``` В случае успешного нахождения `libcrypto` из OpenSSL, будут определены переменные `${OPENSSL_INCLUDE_DIR}` и `${OPENSSL_CRYPTO_LIBRARY}`. ### Workflow Преобразования данных криптографическими функциями подразумевает три стадии: 1. Инициализация - функции, заканчивающиеся на `Init` 2. Добавление очередной порции данных с помощью одной из функций, имя которой заканчивается на `Update`. Этот процесс можно повторять итеративно по мере поступления данных 3. Финализация - функции, оканчивающиеся на `Final`; на этом этапе появляется итоговый результат преобразования. ### Функции `libcrypto` Функции, имена которых начинаются с `SHA` или `MD5` предназначены для вычисления хеш-значений. Они используют простой workflow из трех стадий. Для кодирования или декодирования с использованием симметричного ключа используется стандартный workflow, но на стадии инициализации настраивается *контекст* - переменная, которая хранит состояние шифрующего автомата. Пример: ``` // Создание контекста EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); // Генерация ключа и начального вектора из // пароля произвольной длины и 8-байтной соли EVP_BytesToKey( EVP_aes_256_cbc(), // алгоритм шифрования EVP_sha256(), // алгоритм хеширования пароля salt, // соль password, strlen(password), // пароль 1, // количество итераций хеширования key, // результат: ключ нужной длины iv // результат: начальный вектор нужной длины ); // Начальная стадия: инициализация EVP_DecryptInit( ctx, // контекст для хранения состояния EVP_aes_256_cbc(), // алгоритм шифрования key, // ключ нужного размера iv // начальное значение нужного размера ); ``` ================================================ FILE: practice/posix_dirent_time/README.md ================================================ # POSIX API для работы с файловой системой и временем ## Каталоги Каталоги в UNIX-системах - это специальный вид файлов, который содержит набор пар `{inode, name}` для построения иерархии файловой системы. Как и обычные файлы, каталоги могут быть открыты на чтение или запись с помощью системного вызова `open`. В системе Linux существует не обязательный флаг открытия `O_DIRECTORY`, единственное назначие которого - проверить, что открываемый файл действительно является каталогом, а не файлом другого типа; в противном случае - диагностировать ошибку. ### Функции для работы с каталогами Формат специально файла-каталога зависит от конкретной операционной системы, и абстракцией на уровне POSIX является функции (не системные вызовы!) стандартной библиотеки Си: ``` #include // открытие каталога DIR *opendir(const char *dirname); DIR *fdopendir(int fd); // закрытие каталога int closedir(DIR *dirp); ``` Открытый каталог описывается структурой `DIR`, которая при открытии каталога размещается в куче, и необходимо её освобождать с помощью функции `closedir`. Чтение из потока каталога осуществляется функцией `readdir`, единицей чтения которой является не байт, а элемент структуры `dirent`: ``` struct dirent { ino_t d_ino; // inode файла в файловой системе char d_name[NAME_MAX+1]; // имя файла /* дальше могут быть ещё какие-нибудь нестандартные поля */ }; ``` В системе Linux максимальное имя файла равно 255 (`NAME_MAX`)символам, но при этом, максимальная длина пути к файлу - 4Кб (`PATH_MAX`). Перемещение текущего указателя чтения записей в каталоге осуществляется с помощью функций `seekdir` и `telldir`. Каждый каталог, даже пустой, содержит, как минимум, две записи: * специальный файл `.` - каталог, inode которого совпадает с inode того каталога, в котором он содержится; * специальный файл `..` - каталог, inode которого совпадает с inode каталога на уровень выше, либо корневого каталога, если каталог уровнем выше не существует. Другие функции работы с каталогами: * `getcwd` - получить текущий рабочий каталог; * `chdir` - сменить текущий каталог. ### 'at'-функции Для того, чтобы упростить работу с относительными путями файлов, в современных версиях `glibc` (Linux и FreeBSD) присуствуют функции для открытия файловых дескрипторов относительно открытого каталога: ``` // Аналоги open int openat(int dirfd, const char *pathname, int flags); int openat(int dirfd, const char *pathname, int flags, mode_t mode); // Аналог stat int fstatat(int dirfd, const char *pathname, struct stat *statbuf, int flags); ``` ## Пользователи и группы Информация о пользователях и группах может храниться как в локальном источнике, например в файлах `/etc/passwd` и `/etc/groups`, так и на удаленных серверах, например LDAP. Информацию о пользователе или группе можно получить с помощью одной из функций: * `struct passwd *getpwnam(const char *name)` - получить информацию о пользователе по имени; * `struct passwd *getpwuid(uid_t uid)` - получить информацию о пользователе по его User ID; * `struct group *getgrnam(const char *name)` - получить информацию о группе по имени; * `struct group *getgrgid(gid_t gid)` - получить информацию о группе по её Group ID. ## Работа со временем ### Текущее время Время в UNIX-системах определяется как количество секунд, прошедшее с 1 января 1970 года, причем часы идут по стандартному гринвичскому времени (GMT) без учета перехода на летнее время (DST - daylight saving time). 32-разрядные системы должны прекратить своё нормальное существование 19 января 2038 года, поскольку будет переполнение знакового целого типа для хранения количества секунд. Функция `time` возвращает количество секунд с начала эпохи. Аргументом функции (в который можно передать `NULL`) является указатель на переменную, куда требуется записать результат. В случае, когда требуется более высокая точность, чем 1 секунда, можно использовать системный вызов `gettimeofday`, который позволяет получить текущее время в виде структуры: ``` struct timeval { time_t tv_sec; // секунды suseconds_t tv_usec; // микросекунды }; ``` В этом случае, несмотря на то, что в структуре определено поле для микросекунд, реальная точность будет составлять порядка 10-20 миллисекунд для Linux. Более высокую точность можно получить с помощью системного вызова `clock_gettime`. ### Разложение времени на составляющие Человеко-представимое время состоит из даты (год, месяц, день) и времени суток (часы, минуты, секунды). Это описывается структурой: ``` struct tm { /* время, разбитое на составляющие */ int tm_sec; /* секунды от начала минуты: [0 -60] */ int tm_min; /* минуты от начала часа: [0 - 59] */ int tm_hour; /* часы от полуночи: [0 - 23] */ int tm_mday; /* дни от начала месяца: [1 - 31] */ int tm_mon; /* месяцы с января: [0 - 11] */ int tm_year; /* годы с 1900 года */ int tm_wday; /* дни с воскресенья: [0 - 6] */ int tm_yday; /* дни от начала года (1 января): [0 - 365] */ int tm_isdst; /* флаг перехода на летнее время: <0, 0, >0 */ }; ``` Для преобразования человеко-читаемого времени в машинное используется функция `mktime`, а в обратную сторону - одной из функций: `gmtime` или `localtime`. ### Daylight Saving Time Во многих странах используется "летнее время", когда стрелки часов переводятся на час назад. История введения/отмены летнего времени, и его периоды хранится в [базе данных IANA](https://data.iana.org/time-zones/tz-link.html). База данных представляет собой набор правил в тектовом виде, которые компилируются в бинарное представление, используемое библиотекой glibc. Наборы файлов с правилами перехода на летнее время для разных регионов хранятся в `/usr/share/zoneinfo/`. Когда значение `tm_isdst` положительное, то применяется летнее время, значение `tm_isdst` - зимнее. В случае, когда значение `tm_isdst` отрицательно, - используются данные из timezone data. ## Reentrant-функции Многие функции POSIX API разрабатывались во времена однопроцессорных систем. Это может приводить к разным неприятным последствиям: ``` struct tm * tm_1 = localtime(NULL); struct tm * tm_2 = localtime(NULL); // opps! *tm_1 changed! ``` Проблема заключается в том, что некоторые функции, например `localtime`, возвращает указатель на структуру-результат, а не скалярное значение. При этом, сами данные структуры не требуется удалять, - они хранятся в `.data`-области библиотеки glibc. Проблема решается введением *повторно входимых (reentrant)* функций, которые в обязательном порядке трубуют в качестве одного из аргументов указатель на место в памяти для размещения результата: ``` struct tm tm_1; localtime_r(NULL, &tm_1); struct tm tm_2; localtime_r(NULL, &tm_2); // OK ``` Использование повторно входимых функций является обязательным (но не достаточным) условием при написании многопоточных программ. Некоторые reentrant-функции уже не актуальны в современных версиях glibc для Linux, и помечены как deprecated. Например, реализация `readdir` использует локальное для каждого потока хранение данных. ================================================ FILE: practice/posix_ipc/README.md ================================================ # Средства межпроцессного взаимодействия POSIX ## Разделяемая память Для создания сегментов разделяемой памяти используется механизм [отображаемых файлов mmap](../mmap). Выполнение системного вызова `mmap` с параметрами `MAP_SHARED` и указанием файлового дескпритора позволяет использовать некоторое имя в файловой системе для взаимодействия между собой неродственных процессов. Для того, чтобы избежать создания файлов на диске (которые занимают место), предусмотрен механизм создания ортогонального пространства имен ("ключей") для разделяемых файлов, которые существуют только в памяти. Каждый ключ - это некоторая строка длиной до `NAME_MAX` байт, которая должна начинаться с символа `'/'`. У ключей могут быть права доступа и владелец, по аналогии с обычными файлами. Такие разделяемые объекты существуют до перезагрузки компьютера, либо до их явного удаления одним из процессов. ### Функции для работы с разделяемыми файлами * `int shm_open(const char *name, int flags, mode_t mode)` - по аналогии с системным вызовом `open`, открывает ключ по имени, как обычный файл. Если среди флагов в `flags` присутствует опция `O_CREAT`, то третий аргумент подразумевает права доступа, в противном случае его значение игнорируется. Возвращает файловый дескриптор, который можно передать в системный вызов `mmap`. * `int shm_unlink(const char *path)` - удаляет имя из памяти. Если разделяемый файл был имеет отображение в одном или нескольких процессах, то он продолжает быть доступным и занимать место в памяти. Права доступа можно настраивать с помощью системных вызовов `fchmod` и `fchown`, которые, в отличии от команд shell'а и системных вызовов, работающих с именами, позволяют работать с файловыми дескрипторами. При создании объекта, он имеет размер `0`, и может быть изменен с помощью `ftruncate`. ### Особенности реализации в Linux В операционной системе Linux (в отличии, например, от FreeBSD), объекты разделяемой памяти - это самые обычные файлы, которые располагаются в файловой системе `tmpfs`, примонтированной в `/dev/shm`. Кроме того, есть дополнительные особенности: * для использования функций разделяемых объектов POSIX, нужно указывать опцию `-lrt`, посколько `glibc` разбита на несколько частей; * требование про символ `/` в начале имени ключа является не обязательным; тем не менее, это противоречит стандарту `POSIX` и в BSD системах приводит к ошибке `EINVAL`, а в системе `QNX` просто создаст обычный файл на диске в текущем каталоге. ## Семафоры Семафор - это беззнаковая целочисленная переменная, которая обладает дополнительными свойтсвами: * существуют операции увеличения и уменьшения счетчика, которые выполняются атомарно; * при попытке уменьшить счетчик, который равен `0`, выполняется приостановка процесса (или нити), который пытается это сделать; * при увеличении счетчика, если существует какой-то приостановленный процесс, который пытался его уменьшить, работа этого процесса возобновляется. В теоретической литературе операция *захвата семафора* (уменьшения значения) обычно обозначается буквой `P` (proberen), а операция *освобождения* (увеличение значения) - буквой `V` (verhogen), - названия операций заимствованы из нидерландского языка, т.к. семафоры изобрел Дейкстра. Семафоры часто используют для синхронизации между собой нескольких потоков выполнения, и в частности, они могут использоваться для предотвращения гонки данных. ### Семафоры POSIX Семафоры POSIX определяются некотроым типом `sem_t`, объявленным в файле ``, реализация которого, в общем случае, считается неопределенной, и зависит не только от конкретной операционной системы, но и от процессора. Функции работы с семафорами обычно принимают его по указателю: * `sem_wait(sem_t *sem)` - захватить семафор (операция `P`); * `sem_post(sem_t *sem)` - освободить семафор (операция `V`); * `sem_trywait(sem_t *sem)` - попытаться захватить семафор, если он равен нулю, то процесс не блокируется, а функция вовзращает значение `-1`, прописывая значение `EAGAIN` в `errno`; * `sem_timedwait(sem_t *sem, struct timespec *timeout)` - захватывает семафор, но если за указанный промежуток времени он не был разблокирован, то функция завершает свою работу с ошибкой `ETIMEDOUT`; * `sem_getvalue(sem_t *sem, int *out_var)` - читает численное значение семафора, не блокируя его; эта функция бывает полезна при отладке. В системе Linux для использования функций работы с семафорами необходимо компоновать программу с опцией компилятора `-pthread`. Перед использованием, семафоры должны быть корректно инициализированы. Инициализиция зависит от типа семафора. ### Анонимные семафоры Анонимные семафоры - это семафоры, которые доступны только в рамках одного адресного пространства (для многопоточности), либо родственным процессам. Создаются с попощью функции `sem_init`: ``` int sem_init(sem_t *sem, // указатель на семафор в памяти int pshared, // 0 - если предназначен для использования // в рамках одного адресного пространства, // 1 - если разными процессами unsinged value // начальное значение ) ``` Уничтожаются анонимные семафоры с помощью функции `sem_destroy`. При этом ситуация, когда какие-то процессы или нити были заблокированы семафором, считается **неопределенным поведением**, и может приводить к полной блокировке. Семафоры, которые предназначены для использования разными процессами, должны находиться в разделяемой через `mmap` области памяти, доступной всем задействованным процессам. В противном случае, изменения семафора в одном процессе не будут видны остальным. ### Именованные семафоры Именованные семафоры - это реализация семафоров совместно с механизмом разделяемой памяти POSIX. Имена семафоров подчиняются тем же правилам, что имена сегментов разделяемой памяти, за одним исключением: максимальная длина имени на 4 байта короче, т.к. к имени семафора автоматически приписывается (в зависимости от реализации) суффикс `.sem` или префикс `sem.`. Создаются именованные семафоры с помощью `sem_open`: ``` sem_t *sem_open(const char *name, int oflag); sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); ``` По аналогии с `open`, если присутствует флаг `O_CREAT`, то нужно указать права доступа, и кроме того, - начальное значение. В отличии от обычных файлов, не нужно указывать флаги, определяющие режим открытия на чтение/запись. Если открывается существующий семафор, то `oflag=0`. Закрытие именованного семафора с помощью `sem_close`, в отличии от анонимного, никак не влияет на процессы, которые могут быть им заблокированы: значение счетчика остается неизменным, и может быть изменено после повторного открытия. Применять операцию `sem_destroy` для именованных семафоров запрещено, так же как и операцию `sem_close` - для анонимных. Для удаления имени семафора используется функция `sem_unlink(const char *name)`. Как и в случае обычного файла, значение семафора сохраняется даже после удаления до тех пор, пока он не будет закрыт всеми использующими его процессами. ================================================ FILE: practice/pthread/README.md ================================================ # Многопоточность ## Общие сведения Поток (нить, легковесный процесс) - единица планирования времени в рамках одного процесса. Все потоки в рамках одного процесса разделяют общее адресное пространство и открытые файловые дескрипторы. Для каждого потока предусмотрен свой отдельный стек фиксированного размера, который располагается в общем адресном пространстве. В конце стека для каждого потока обычно присутствует небольшой участок памяти (Guad Page), предназначенный для того, чтобы предотвратить ситуацию перезаписи данных другого потока в результате, например, его переполнения. В каждом процессе существует как минимум один поток, выпонение которого начинается с функции `_start`. В отличии от обычных процессов, которые имеют иерархическую структуру "родитель-ребенок", все потоки являются равнозначными. ## POSIX Threads Стандартом для UNIX-систем является POSIX Threads API. В системе Linux (как и во FreeBSD), ввиду фрагментации стандартной библиотеки, компоновка программ должна проводиться с опцией компилятора `-pthread`. В отличии от большинства других функций POSIX, в случае ошибки, функции из pthread не прописывают их код в переменную `errno`, а возвращают различные целочисленные значения, отличные от `0`, которые соответствуют определенным ошибкам. ### Создание и запуск нового потока ``` int pthread_create( // указатель на переменную-результат pthread_t *thread, // опционально: параметры нового потока, // может быть NULL const pthread_attr_t *attr, // функция, которая будет выполняться (void*)(*function)(void*), // аргумент, который передается в функцию void *arg ); ``` Функция `pthread_create` создает новый поток, и сразу же запускает в нем на выполнение функцию, которая передана в качестве аргумента. Выполняемая функция должна принимать единственный аргумент размером с машинное слово (`void*`), и этот аргумент передается одновременно с созданием потока. Возвращаемое значение выполняемой функции можно будет получить после её выполнения о ожидания завершения потока. ### Завершение работы потока и результат работы Поток завершается в тот момент, когда завершается выполнение функции, либо пока не будет вызван аналог `exit` для потока - функция `pthread_exit`. Возвращаемые значения размером больше одного машинного слова, которые являются результатом работы потока, не могут быть размещены в стеке, поскольку стек будет уничтожен при завершении работы функции. Дождаться завершения потока и получить результат можно с помощью функции `pthread_join` ``` int pthread_join( // поток, который нужно ждать pthread_t thread, // указатель на результат работы функции, // либо NULL, если он не интересен (void*) *retval ); ``` Функция `pthread_join` ожидает завершения работы определенного потока, и получает результат работы функции. Возможна ситуация, приводящая к deadlock'у, когда два потока вызывают друг для друга ожидание. Функция `pthread_join` проверяет эту ситуацию, и завершается с ошибкой (не блокируя выполнение). ``` pthread_t a; pthread_t b; void* thread_func_a(void *) { sleep(1); pthread_join(b, NULL); } void* thread_func_b(void *) { sleep(1); pthread_join(a, NULL); } // Bug: Deadlock, but detected pthread_create(&a, NULL, thread_func_a, 0); pthread_create(&b, NULL, thread_func_b, 0); ``` Такая проверка возможна только при попытке ожидать поток, который уже ожидает поток, пытающийся вызвать `pthread_join`. В случае нескольких потоков, которые косвенно ожидают друг друга, такая диагностика невозможна, и приведет к deadlock'у. ### Принудительное завершение потока Функция `pthread_cancel` принудительно завершает работу потока, если поток явно это не запретил с помощью функции `pthread_setcancelstate`. ``` int pthread_cancel( // поток, который нужно прибить pthread_t thread ); ``` Результатом работы функции, который будет передан в `pthread_join` будет специальное значение `PTHREAD_CANCELED`. В системе Linux остановка потоков реализована через отправку процессом самому себе сигнала реального времени с номером `32`. Принудительное завершение потока вовсе не означает, что поток будет немедленно остановлен. Функция `pthread_cancel` только проставляет флаг остановки, и этот флаг может быть проверен только во время определенного набора системных вызовов и функций стандартной библиотеки, которые называются *Cancelation Points*. Полный список функций, которые могут быть прерваны, перечислен в разделе `7` man-страницы `pthreads`. Некоторые системы, в том числе Linux, позволяют принудительно завершить поток даже вне Cancelation Points. Для этого поток должен вызывать функцию `pthread_setcanceltype` с параметром `PTHREAD_CANCEL_ASYNCHRONOUS`. После этого завершение потока будет осуществляться на уровне планировщика заданий. ### Атрибуты потока Атрибуты потока (второй параметрв в `pthread_create`) хранятся в структуре `pthread_attr_t`, объявление которой является платформо-зависимым, и не регламентируется стандартом POSIX. Для инициализации атрибутов используется функция `pthread_attr_init(pthread_attr_t *attr)`, и кроме того, после использования, структуру атрибутов необходимо уничтожать с помощью `pthread_attr_destroy`. С помощью нескольких функций-сеттеров можно задавать определенные параметры вновь создаваемого потока: * `pthread_attr_setstacksize` - установить размер стека для потока. Размер стека должен быть кратен размеру страницы памяти (обычно 4096 байт), и для него определен минимальный размер, определяемый из параметров системы `sysconf(_SC_THREAD_STACK_MIN)` или константой `PTHREAD_STACK_MIN` из `` (в Linux это 16384 байт); * `pthread_attr_setstackaddr` - указать явным образом адрес размещения памяти, которая будет использована для стека; * `pthread_attr_setguardsize` - установить размер защитной области после стека (Guard Page). По умолчанию в Linux этот размер равен размеру страницы памяти, но можно явно указать значение 0. ================================================ FILE: practice/python/README.md ================================================ # Python: расширение и внедрение Основной источник (на английском): [Extending and Embedding](https://docs.python.org/3.6/extending/index.html). Справочная информация (на английском): [Python/C API](https://docs.python.org/3.6/c-api/index.html). ## Ипользование интерпретатора Python Интерпретатор Python реализован в разделяемой библиотеке, а исполняемый файл интерпретатора `python3` является лишь оболочкой для запуска интерпретатора. Для сборки программы можно использовать CMake, в стандартной поставке которого входит поддержка Python. ```cmake find_package(PythonLibs 3.6 REQUIRED) include_directories(${PYTHON_INCLUDE_DIRS}) target_link_libraries(program ${PYTHON_LIBRARIES}) ``` Обратите внимание на то, что необходимо указывать минимальную версию интерпретатора, поскольку в поставку многих дистрибутивах Linux входит две версии Python (2.7 и 3.x), и может возникнуть неоднозначность используемой библиотеки. Тривиальная реализация своего интерпретатора, с использованием библиотеки `python` выглядит следующим образом: ```c #include // В этом заголовочном файле собран почти весь API Python #include int main(int argc, char *argv[]) { // Открытие файла на чтение FILE* fp = fopen(argv[1], "r"); // Инициализация интерпретатора Python Py_Initialize(); // Выполнение файла PyRun_SimpleFile(fp, argv[1]); // Завершение работы интерпретатора Py_Finalize(); // Закрытие файла fclose(fp); } ``` Указание имени файла в качестве второго аргумента является желательным по двум причинам: оно используется при генерации сообщении возникающих исключительных ситуаций, и кроме того, используется для определения пути поиска зависимых модулей. Переданный текст доступен через глобальную переменную `__file__` и может быть произвольным. ```python /* Си */ PyRun_SimpleFile(fp, "abrakadabra"); # Python print(__file__) # abrakadabra ``` Скрипт на языке Python может иметь аргументы командной строки, которые доступны через переменную-список `sys.argv`. В приведенной выше тривиальной реализации интерпретатора это значение не установлено, поэтому обращение к `sys.argv` выдаст ошибку о том, что эта переменная не определена: ``` AttributeError: module 'sys' has no attribute 'argv' ``` Перед запуском файла на выполнение можно установить список аргументов с помощью `PySys_SetArgv`: ```c wchar_t* args[] = { L"One", L"Two", L"Аргумент" }; PySys_SetArgv(3, args); // int argc, wchar_t *argv[] ``` Обратите внимание на то, что строки в Python являются многобайтовыми, поэтому многие функции API подразумевают работу с типом данных `wchar_t`. В случае использования однобайтных цепочек символов используется системная локаль, как правило это UTF-8. Программой может быть не только текст программы, хранящийся в файле, но и произвольная строка текста: ```c PyRun_SimpleString("a=1\nb=2\nprint(a+b)"); // будет выведено 3 ``` При выполнении текста, с которым не связан никакой файл, глобальная переменная `__file__` считается не определенной, а в случае возникновения исключений, в качестве файла-источника будет использован текст ``. ```c PyRun_SimpleString("print(__file__)"); Traceback (most recent call last): File "", line 1, in NameError: name '__file__' is not defined ``` ## API стандартных классов Python Язык Python содержит реализацию стандартных контейнерных классов, которые являются встроенными типами Python, но их можно использовать и без интерпретации текста программы. Базовым классом для всех объектов является класс `object`, которому в Си API соответствует базовый класс `PyObject`. Поскольку интерптетатор реализован на языке Си, который не является объектно-ориентированным, то на пользователя API возлагается ответственноть за контролем используемых типов. Все классы и методы в PyObject API именуются следующим образом:`PyКласс_Метод`, а указатель на объект класса передается в качестве первого аргумента. Примеры стандартных классов и методов: * `PyList_Append(PyObject *list, PyObject *item)` - эквивалент `list.append(item)` * `PyDict_SetItem(PyObject *p, PyObject *key, PyObject *val)` - эквивалент `p[key]=val` Обратите внимание, что для объектов всех классов используется тип `PyObject*`, поэтому необходимо предварительно проверять, какой тип имеет переменная с помощью одной из функций вида `PyКласс_Check(PyObject *p)`, которая возвращает ненулевое значение в случае принадлежности объекта к классу, и `0` в противном случае. Соответствие стандартных типов Python префиксам функций PyObject API: `list` - `PyList_`, `tuple` - `PyTuple_`, `dict` - `PyDict_`, `str` - `PyUnicode_`, `bytes` - `PyBytes_`, `file` - `PyFile_`, `int` - `PyLong_`, `float` - `PyFloat_`. Обратите внимание, что тип для строк называется `PyUnicode`, а не `PyString`. Это связано с тем, что во времена Python 2 строки были двух видов: однобайтные и юникодные, а в Python 3 остались только юникод-строки. **Пример использования API без интерпретатора**: разбить текст на лексемы, выделяя целые числа как числа, а остальные слова оставляя строками. Этому коду соответствует программа на Python: ```python text = "сейчас 23 59 не время спать" result = [] tokens = text.split(" ") for entry in tokens: try: number = int(entry) result += [number] except: result += [ entry.upper() ] print(result) # ['сейчас', 23, 59, 'не', 'время', 'спать'] ``` ```c int main() { // Если не используются wchar_t*, то по умолчанию подразумевается, // что все однобайтные строки - в кодировке UTF-8 static const char TheText[] = "сейчас 23 59 не время спать"; // Инициализация API Python Py_Initialize(); // Создание Python-строк из Си-строк PyObject *py_text = PyUnicode_FromString(TheText); PyObject *py_space_symbol = PyUnicode_FromString(" "); // Создание пустого списка PyObject *py_result = PyList_New(0); // str.split(py_text, py_space_symbol, maxsplit=-1) PyObject *py_tokens = PyUnicode_Split(py_text, py_space_symbol, -1); PyObject *py_entry = NULL; PyObject *py_number = NULL; // Цикл по элементам списка. PyList_Size - его размер for (int i=0; iob_refcnt=1 return ret; // OK } # из Python: a = func_returning_string() # ret -> a, refcnt=1 func_returning_string() # del ret, refcnt=1 --> refcnt=0 ``` В случае использования `None`, поскольку он существует в единственном экземпляре, нужно увеличить количество ссылок: ```c static PyObject * func_returning_none(PyObject *self) { // Py_None - это указатель на статический объект _Py_NoneStruct Py_INCREF(Py_None); // Py_None->ob_refcnt ++ return Py_None; } ``` ## Реализация модулей для использования штатным интерпретатором Си-модуль для языка Python - это разделяемая библиотека, которая загружается через механизм `dlopen`, и поэтому должна быть скомпилирована в позиционно-независимый код (опция `-fPIC` компилятора). Библиотека имеет не стандартное имя файла: * `МОДУЛЬ.so` - для Mac, Linux и *BSD. Обратите внимание на отсутствие префикса `lib` в имени, и кроме того, в Mac используется суффикс `.so` вместо `.dynlib` * `МОДУЛЬ.pyd` или `МОДУЛЬd.pyd` - для Windows. Вместо суффикса `.dll` используется `.pyd` или `d.pyd` (для варианта сборки с отладочной информацией). Единственная функция, которая обязана быть реализована в библиотеке - это функция: ```c PyObject* PyInit_МОДУЛЬ(); ``` Функция должна создавать и возвращать объект модуля, по аналогии с расширением интерпретатора встроенным модулем. Если код модуля реализуется на языке C++, то необходимо отключить преобразование имен с помощью `extern "C"`, а в случае с операционной системой Windows и компилятором MSVC - ещё и объявить функцию экспортируемой: `__declspec(dllexport)`. Все платформо-зависимые объявления спрятаны в макрос `PyMODINIT_FUNC`, значение которого определяется препроцессором. Таким образом, модуль реализуется функцией: ```c PyMODINIT_FUNC PyInit_my_great_module() { static PyModuleDef modDef = { .m_base = PyModuleDef_HEAD_INIT, .m_name = "my_great_module", .... } return PyModule_Create(&modDef); } ``` Обратите внимание, что имя файла с модулем, имя части функции после `PyInit_` и имя самого модуля должны совпадать, иначе интерпретатор не сможет найти и загрузить его. Все остальные функцие модуля могут быть статическими, а не экспортироваться из библиотеки, поскольку указатели на них явным образом будут присутствовать в объекте, который вернет функция инициализации. В CMake-пакете `PythonLibs` определяется функция для создания цели-модуля: ```cmake find_package(PythonLibs 3.6 REQUIRED) python_add_module(my_great_module module.c) ``` Эта цель определяет необходимые опции компиляции для сборки позиционно-независимого кода с правильным именем, независимо от используемой операционной системы. Для загрузки модуля из Python необходимо разместить его в одном из каталогов поиска модулей Python, либо рядом с файлом скрипта, который его использует. Python при этом корректно работает с символическими ссылками. ## Python и отладчик GDB Отладчик GDB позволяет ставить точки останова в любой части программы, для которой существует отладочная информация. Таким образом, если собрать модуль отдельно с опцией `-g`, то вместе с ним можно использовать `gdb` даже в том случае, если для самого интерпретатора отладочная информация отсутствует. Целевой программой для `gdb` указывается исполняемый файл интерпретатора `python3` , а сам тестовый скрипт - в качестве аргумента запуска. ```bash > gdb python3 (gdb) b module.c:112 (gdb) r script.py ``` Современные версии `gdb` (начиная с 7.x) включают поддержку расширений для типов данных Python API, и использование команды отладчика `print` вызывает метод `repr` языка Python для выводимых объектов, а он, в свою очередь - подразумевает вызов функции `PyObject_Repr(PyObject *obj)` для произвольного Python-объекта. В случае, если переменная не инициализирована, и содержит мусор по указателю, то это может приводить к ошибке нарушения сегментации, причиной которой становится сам отладчик, вызывая `print`. При использовании интегрированных сред разработки, эта команда отладчика вызывается очень часто для обновления значений локальных переменных, что может приводить к печальным последствиям. ```c PyObject* some_function(PyObject *self, PyObject *args) { // <-- точка останова где-то здесь .... .... PyObject * value = ... ... } ``` В данном примере отладчик инициирует ошибку нарушения сегментации, поскольку локальная переменная `value` ещё не инициализирована. Для того, чтобы этого избежать, есть два способа: * Отключить использования вызова `repr` для Python-объектов командой отладчика `disable pretty-printer` (в среде QtCreator это делается автоматически при снятии чекбокса "Use Debugging Helper" в настройках отладчика), - в этом случае все Python-объекты будут отображаться как Си-структуры. * Реорганизовать код таким образом, чтобы на момент остановки отладчиком все локальные переменные были инициализированы. ```c PyObject* some_function(PyObject *self, PyObject *args) { PyObject * value = NULL; .... // <-- точка останова после инициализации всех PyObject* .... value = ... ... } ``` ================================================ FILE: practice/signal-1/README.md ================================================ # Сигналы. Часть 1 ## Введение Сигнал - это механизм передачи коротких сообщений (номер сигнала), как правило, прерывающий работу процесса, которому он был отправлен. Сигналы могут быть посланы процессу: * ядром, как правило, в случае критической ошибки выполнения; * другим процессом; * самому себе. Номера сигналов начинаются с 1. Значение 0 имеет специальное назначение (см. ниже про `kill`). Некоторым номерам сигналов соответствуют стандартные для POSIX названия и назначения, которые подробно описаны `man 7 signal`. При получении сигнала процесс может: 1. Игнорировать его. Это возможно для всех сигналов, кроме `SIGSTOP` и `SIGKILL`. 2. Обработать отдельной функцией. Кроме `SIGSTOP` и `SIGKILL`. 3. Выполнить действие по умолчанию, предусмотренное назначением стандартного сигнала POSIX. Как правило, это завершение работы процесса. По умолчанию, все сигналы, кроме `SIGCHILD` (информирование о завершении дочернего процесса) и `SIGURG` (информировании о поступлении TCP-сегмента с приоритетными данными), приводят к завершению работы процесса. Если процесс был завершён с помощью сигнала, а не с ипользованием системного вызова `exit`, то для него считается не определенным код возврата. Родительский процесс может отследить эту ситуацию, используюя макросы `WIFSIGNALED` и `WTERMSIG`: ``` pid_t child = ... ... int status; waitpid(child, &status, 0); if (WIFEXITED(status)) { // дочерний процесс был завершён через exit int code = WEXITSTATUS(status); // код возврата } if (WIFSIGNALED(status)) { // дочерний процесс был завершёл сигналом int signum = WTERMSIG(status); // номер сигнала } ``` Отправить сигнал любому процессу можно с помощью команды `kill`. По умолчанию отправляется сигнал `SIGTERM`, но можно указать в качестве опции, какой именно сигнал нужно отправить. Кроме того, некоторые сигналы отправляются терминалом, например Ctrl+C посылает сигнал `SIGINT`, а Ctrl+\ - сигнал `SIGQUIT`. ## Пользовательские сигналы Изначально в POSIX было зарезервировано два номера сигнала, которые можно было использовать на умотрение пользователя: `SIGUSR1` и `SIGUSR2`. Кроме того, в Linux предусмотрен диапазон сигналов с номерами от `SIGRTMIN` до `SIGRTMAX`, которые можно использовать на усмотрение пользователя. Действием по умолчанию для всех "пользовательских" сигналов является завершение работы процесса. ## Отправка сигналов программным способом ### Системный вызов `kill` По аналогии с одноимённой командой, `kill` предназначен для отправки сигнала любому процессу. ``` int kill(pid_t pid, int signum); // возврашает 0 или -1, если ошибка ``` Отправлять сигналы можно только тем процессам, которые принадлежат тому пользователю, что и пользователь, по которым выполняется системный вызов `kill`. Исключение составляет пользователь `root`, который может всё. При попытке отправить сигнал процессу другого пользователя, `kill` вернёт значение `-1`. Номер процесса может быть меньше `1` в случаях: * `0` - отправить сигнал всем процессам текущей группы процессов; * `-1` - отправить сигнал всем процессам пользователя (использовать с осторожностью!); * отрицательное значение `-PID` - отправить сигнал всем процессам группы `PID`. Номер сигнала может принимать значение `0`, - в этом случае никакой сигнал не будет отправлен, а `kill` вернёт значение `0` в том случае, если процесс (группа) с указанным `pid` существует, и есть права на отправку сигналов. ### Функции `raise` и `abort` Функция `raise` предназначен для отправки сигнала процессом самому себе. Функция стандартной библиотеки `abort` посылает самому себе сигнал `SIGABRT`, и часто используется для генерации исключительных ситуаций, которые получилось диагностировать во время выполнения, например, функцией `assert`. ### Системный вызов `alarm` Системный вызов `alarm` запускает таймер, по истечении которого процесс сам себе отправит сигнал `SIGALRM`. ``` unsigned int alarm(unsigned int seconds); ``` Отменить ранее установленный таймер можно, вызвав `alarm` с параметром `0`. Возвращаемым значением является количество секунд предыдущего установленного таймера. ## Обработка сигналов Сигналы, которые можно перехватить, то есть все, кроме `SIGSTOP` и `SIGKILL`, можно обработать программным способом. Для этого необходимо зарегистрировать функцию-обработчик сигнала. ### Системный вызов `signal` ``` #include // Этот тип определен только в Linux! typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); // для Linux void (*signal(int signum, void (*func)(int))) (int); // по стандарту POSIX ``` Системный вызов `signal` предназначен для того, чтобы зарегистрировать функцию в качестве обработчика определенного сигнала. Первым аргументом является номер сигнала, вторым - указатель на функцию, которая принимает единственный аргумент - номер пришедшего сигнала (т.е. одну функцию можно использовать сразу для нескольких сигналов), и ничего не возвращает. Два специальных значения функции-обработчика `SIG_DFL` и `SIG_IGN` предназанчены для указания обработчика по умолчанию (т.е. отмены ранее зарегистрированного обработчика) и установки игнорирования сигнала. Системный вызов `signal` возвращает указатель на ранее установленный обработчик. ### System-V v.s. BSD В стандартах, родоначальниками которых были UNIX System-V и BSD UNIX, используется различное поведение обработчика сигнала, зарегистрированного с помощью `signal`. При определении одного из макросов препроцессора: `_BSD_SOURCE`, `_GNU_SOURCE` или `_DEFAULT_SOURCE` (что подразумевается опцией компиляции `-std=gnu99` или `-std=gnu11`), используется семантика BSD; в противном случае (`-std=c99` или `-std=c11`) - семантика System-V. Отличия BSD от System-V: * В System-V обработчик сигнала выполяется один раз, после чего сбрасывается на обработчик по умолчанию, а в BSD - остается неизменным. * В BSD обработчик сигнала не будет вызван, если в это время уже выполняется обработчик того же самого сигнала, а в System-V это возможно. * В System-V блокирующие системные вызовы (например, `read`) завершают свою работу при поступлении сигнала, а в BSD большинство блокирующих системных вызовов возобновляют свою работу после того, как обработчик сигнала заверщает свою работу. По этой причине, системный вызов `signal` считается устаревшим, и в новом коде использовать его запрещено, за исключением двух ситуаций: ``` signal(signum, SIG_DFL); // сброс на обработчик по умолчанию signal(signum, SIG_IGN); // игнорирование сигнала ``` ### Системный вызов `sigaction` Системный вызов `sigaction`, в отличии от `signal`, в качестве второго аргумента принимает не указатель на функцию, а указатель на структуру `struct sigaction`, с которой, помимо указателя на функцию, хранится дополнительная информация, описывающая семантику обработки сигнала. Поведение обработчиков, зарегистрированных с помощью `sigaction`, не зависит от операционной системы. ``` int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *oldact); ``` Третьим аргументов является указатель на структуру, описывающую обработчик, который был зарегистрирован для этого. Если эта информация не нужна, то можно передать значение `NULL`. Основные поля структуры `struct sigaction`: * `sa_handler` - указатель на функцию-обработчик с одним аргументом типа `int`, могут быть использованы значения `SIG_DFL` и `SIG_IGN`; * `sa_flags` - набор флагов, опиывающих поведение обработчика; * `sa_sigaction` - указатель на функцию-обработчик с тремя параметрами, а не одним (используется, если в флагах присутствует `SA_SIGINFO`). Некоторые флаги, которые можно передавать в `sa_flags`: * `SA_RESTART` - продолжать выполнение прерванных системных вызовов (семантика BSD) после завершения обработки сигнала. По умолчанию (если флаг отсутствует) используется семантика System-V. * `SA_SIGINFO` - вместо функции из `sa_handler` нужно использовать функцию с тремя параметрами `int signum, siginfo_t *info, void *context`, которой помимо номера сигнала, передается дополнительная информация (например PID отправителя) и пользовательский контекст. * `SA_RESETHAND` - после выполнения обработчика сбросить на обработчик по умолчанию (семантика System-V). По умолчанию (если флаг отсутствует) используется семантика BSD. * `SA_NODEFER` - при повторном приходе сигнала во время выполени обработчика он будет обработан немедленно (семантика System-V). По умолчанию (если флаг отсутствует) используется семантика BSD. ## Асинхронность обработки сигналов Сигнал может прийти процессу в любой момент времени. При этом, выполнение текущего кода будет прервано, и будет запущен обработчик сигнала. Таким образом, возникает проблема "гонки данных", которая часто встречается в многопоточном программировании. Существует безопасный целочисленный (32-разрядный) тип данных, для которого гарантируется атомарность чтения/записи при переключении между выполнением основной программы и выполнением обработчика сигнала: `sig_atomic_t`, объявленный в ``. Кроме того, во время выполнения обработчика сигналов запрещено использовать не потоко-безопасные функции (большинство функций стандартной библиотеки). В то же время, использование системных вызовов - безопасно. ================================================ FILE: practice/signal-2/README.md ================================================ # Сигналы. Часть 2 ## Механизм доставки сигналов С каждым процессом связан аттрибут, который не наследуется при `fork`, - это *маска сигналов, ожидающих доставки*. Как правило, она представляется внутри системы в виде целого числа, хотя стандартом внутреннее представление не регламентируется. Отдельные биты в этой маске соответствуют отдельным сигналам, которые были отправлены процессу, но ещё не обработаны. Поскольку одним битом можно закодировать только бинарное значение, то учитывается только сам факт поступления сигнала, но не их количество. Например, это может быть критичным, если сигналы долго не обрабатываются. Таким образом, использовать механизм стандартных сигналов для синхронизации двух процессов - нельзя. Тот факт, что сигнал оказался в маске ожидающих доставки, ещё не означает, что он будет немедленно обработан. У процесса (или даже у отдельной нити) может существовать маска *заблокированных* сигналов, которая накладывается на маску ожидающих доставки с помощью поразрядной операции `И-НЕ`. В отличии от маски ожидающих достаки, маска заблокированных сигналов наследуется при `fork`. ## Множества сигналов Множества сигналов описываются типом данных `sigset_t`, объявленным в заголовочном файле ``. Операции над множествами: * `sigemptyset(sigset_t *set)` - инициализировать пустое множество; * `sigfillset(sigset_t *set)` - инициализировать полное множество; * `sigaddset(sigset_t *set, int signum)` - добавить сигнал к множеству; * `sigdelset(sigset_t *set, int signum)` - убрать сигнал из множества; * `sigismember(sigset_t *set, int signum)` - проверить наличие сигнала в множестве. ## Блокировка достаки сигналов Временная блокировка доставки сигналов часто используется для защиты критических секций программы, когда внезапное выполнение обработчика может повредить целостности данных или нарушению логики поведения. При этом, нельзя заблокировать сигналы `SIGSTOP` и `SIGKILL`. Блокировка реализуется установки маски блокируемых сигналов с помощью системного вызова `sigprocmask`: ``` int sigprocmask(int how, sigset_t *set, sigset_t *old_set); ``` где `old_set` - куда записать старую маску (может быть `NULL`, если не интересно), а параметр `how` - это одно из значений: * `SIG_SETMASK` - установить множество сигналов в качестве маски блокируемых сигналов; * `SIG_BLOCK` - добавить множество к маске блокируемых сигналов; * `SIG_UNBLOCK` - убрать множество из маски блокируемых сигналов. ## Отложенная обработка сигналов Сигналы, которые попали в маску сигналов, ожидающих доставки, остаются там до тех пор, пока не будут доставлены (а в дальнейшем - либо игнорированы, либо обработаны). Если сигнал был заблокирован, то его обработчик будет вызван сразу после разблокировки. ``` #include #include static void handler(int signum) { static const char Message[] = "Got Ctrl+C\n"; write(1, Message, sizeof(Message)-1); } int main() { sigaction(SIGINT, &(struct sigaction) {.sa_handler=handler, .sa_flags=SA_RESTART}, NULL); sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); while (1) { sigprocmask(SIG_BLOCK, &mask, NULL); sleep(10); sigprocmask(SIG_UNBLOCK, &mask, NULL); } } ``` В данном примере [sigprocmask.c](sigprocmask.c) обработчик сигнала `SIGINT` всё равно будет выполнен, даже несмотря на длительную паузу. ## Временная замена маски заблокированных сигналов Маска сигналов может быть временно заменена. ### Системный вызов `sigsuspend` Системный вызов `sigsuspend(sigset_t *temp_mask)` временно приостанавливает работу программы до тех пор, пока не прийдёт один из сигналов, отсутсвующий в множестве `temp_mask`. Сигналы, отсутсвующие в новом временном множестве, будут доставлены даже в том случае, если они ранее были заблокированы. Сразу после завершения работы `sigsuspend`, маска заблокированных сигналов вернется в исходную. ### Во время обработки сигнала, зарегистрированного `sigaction` Одно из полей структуры `sigaction` определяет маску сигналов, доставка которых будет заблокирована на время выполнения обработчика. Дополнительные флаги при этом не требуются. ``` struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = handler; act.sa_flags = SA_RESTART; sigfillset(&act.sa_mask); // блокировать все сигналы ``` ## Сигналы реального времени Сигналы реального времени - это расширение POSIX, которые, в отличии от стандартных UNIX-сигналов могут быть обработаны используя очередь доставки, и таким образом: * учитывается их количество и порядок прихода; * вместе с сигналом сохраняется дополнительная метаинформация, включая одно челочисленное поле, которое может быть использовано произвольным образом. Сигналы реального времени задаются значениями от `SIGRTMIN` до `SIGRTMAX`, и могут быть использованы с помощью `kill` как дополнительные стандартные UNIX-сигналы. Действие по умолчанию аналогично `SIGTERM`. Для использования очереди сигналов, необходимо отправлять их с помощью функции `sigqueue`: ``` #include union sigval { int sival_int; void* sival_ptr; }; int sigqueue(pid_t pid, int signum, const union sigval value); ``` Эта функция может завершиться с ошибкой `EAGAIN` в том случае, если исчерпан лимит на количество сигналов в очереди. Опциональное значение, передаваемое в качестве третьего параметра, может быть извлечено получателем из поля `si_value` структуры `siginfo_t`, если использовать вариант обработчика `sigaction` с тремя аргументами. ================================================ FILE: practice/signal-2/sigprocmask.c ================================================ #include #include static void handler(int signum) { static const char Message[] = "Got Ctrl+C\n"; write(1, Message, sizeof(Message)-1); } int main() { sigaction(SIGINT, &(struct sigaction) {.sa_handler=handler, .sa_flags=SA_RESTART}, NULL); sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); while (1) { sigprocmask(SIG_BLOCK, &mask, NULL); sleep(10); sigprocmask(SIG_UNBLOCK, &mask, NULL); } } ================================================ FILE: practice/sockets-tcp/README.md ================================================ # Сокеты с установкой соединения ## Сокет Сокет - это файловый дескриптор, открытый как для чтения, так и для записи. Предназначен для взаимодействия: * разных процессов, работающих на одном компьютере (*хосте*); * разных процессов, работающих на разных *хостах*. Создается сокет с помощью системного вызова `socket`: ``` #include #include int socket( int domain, // тип пространства имён int type, // тип взаимодействия через сокет int protocol // номер протокола или 0 для авто-выбора ) ``` Механизм сокетов появился ещё в 80-е годы XX века, когда не было единого стандарта для сетевого взаимодействия, и сокеты являлись абстракцией поверх любого механизма сетевого взаимодействия, поддерживая огромное количество разных протоколов. В современных системах используемыми можно считать несколько механизмов, определяющих пространство имен сокетов; все остальное - это legacy, которое мы дальше рассматривать не будем. * `AF_UNIX` (`man 7 unix`) - пространство имен локальных UNIX-сокетов, которые позволяют взаимодействовать разным процессам в пределах одного компьютера, используя в качестве адреса уникальное имя (длиной не более 107 байт) специального файла. * `AF_INET` (`man 7 ip`) - пространство кортежей, состоящих из 32-битных IPv4 адресов и 16-битных номеров портов. IP-адрес определяет хост, на котором запущен процесс для взаимодействия, а номер порта связан с конкретным процессом на хосте. * `AF_INET6` (`man 7 ipv6`) - аналогично `AF_INET`, но используется 128-разрядная адресация хостов IPv6; пока этот стандарт поддерживается не всеми хостерами и провайдерами сети Интернет. * `AF_PACKET` (`man 7 packet`) - взаимодействие на низком уровне. Через сокеты обычно происходит взаимодействие одним из двух способов (указывается в качестве второго параметра `type`): * `SOCK_STREAM` - взаимодействие с помощью системных вызовов `read` и `write` как с обычным файловым дескриптором. В случае взаимодействия по сети, здесь подразумевается использование протокола `TCP`. * `SOCK_DGRAM` - взаимодейтсвие без предвариательной установки взаимодействия для отправки коротких сообщений. В случае взаимодействия по сети, здесь подразумевается использование протокола `UDP`. ## Пара сокетов Иногда сокеты удобно использовать в качестве механизма взаимодействия между разными потоками или родственными процессами: в отличии от каналов, они являются двусторонними, и кроме того, поддерживают обработку события "закрытие соединения". Пара сокетов создается с помощью системного вызова `socketpair`: ``` int socketpair( int domain, // В Linux поддерживатся только AF_UNIX int type, // SOCK_STREAM или SOCK_DGRAM int protocol, // Только значение 0 в Linux int sv[2] // По аналогии с pipe, массив из двух int ) ``` В отличии от неименованных каналов, которые создаются системным вызовом `pipe`, для пары сокетов не имеет значения, какой элемент массива `sv` использовать для чтения, а какой - для записи, - они являются равноправными. ## Использование сокетов в роли клиента Сокеты могут участвовать во взаимодействии в одной из двух ролей. Процесс может быть *сервером*, то есть объявить некоторый адрес (имя файла, или кортеж из IP-адреса и номера порта) для приема входящих соединений, либо выступать в роли *клиента*, то есть подключиться к какому-то серверу. Сразу после создания сокета, он ещё не готов к взамиодействию с помощью системных вызовов `read` и `write`. Установка взаимодействия с сервером осуществляется с помощью системного вызова `connect`. После успешного выполнения этого системного вызова - взаимодействие становится возможным до выполнения системного вызова `shutdown`. ``` int connect( int sockfd, // файловый дескриптор сокета const struct sockaddr *addr, // указатель на *абстрактную* // структуру, описывающую // адрес подключения socklen_t addrlen // размер реальной структуры, // которая передается в // качестве второго параметра ) ``` Поскольку язык Си не является объектно-ориентированным, то нужно в качестве адреса передавать: 1. Структуру, первое поле которой содержит целое число со значением, совпадающим с `domain` соответствующего сокета 2. Размер этой структуры. Конкретными стурктурами, которые "наследуются" от абстрактной структуры `sockaddr` могут быть: 1. Для адресного пространства UNIX - стрктура `sockaddr_un` ``` #include #include struct sockaddr_un { sa_family_t sun_family; // нужно записать AF_UNIX char sun_path[108]; // путь к файлу сокета }; ``` 2. Для адресации в IPv4 - структура `sockaddr_in`: ``` #include #include struct sockaddr_in { sa_family_t sin_family; // нужно записать AF_INET in_port_t sin_port; // uint16_t номер порта struct in_addr sin_addr; // структура из одного поля: // - in_addr_t s_addr; // где in_addr_t - это uint32_t }; ``` 3. Для адресации в IPv6 - структура `sockaddr_in6`: ``` #include #include struct sockaddr_in6 { sa_family_t sin6_family; // нужно записать AF_INET6 in_port_t sin6_port; // uint16_t номер порта uint32_t sin6_flowinfo; // дополнительное поле IPv6 struct in6_addr sin6_addr; // структура из одного поля, // объявленного как union { // uint8_t [16]; // uint16_t [8]; // uint32_t [4]; // }; // т.е. размер in6_addr - 128 бит uint32_t sin6_scope_id; // дополнительное поле IPv6 }; ``` ## Адреса в сети IPv4 Адрес хоста в сети IPv4 - это 32-разрядное беззнаковое целое число в *сетевом порядке байт*, то есть Big-Endian. Для номеров портов - аналогично. Конвертация порядка байт из сетевого в системный и наоборот осуществляется с помощью одной из функций, объявленных в ``: * `uint32_t htonl(uint32_t hostlong)` - 32-битное из системного в сетевой порядок байт; * `uint32_t ntohl(uint32_t netlong)` - 32-битное из сетевого в системный порядок байт; * `uint16_t htons(uint16_t hostshort)` - 16-битное из системного в сетевой порядок байт; * `uint16_t ntohs(uint16_t netshort)` - 16-битное из сетевого в системный порядок байт. IPv4 адреса обычно записывают в десятичной записи, отделяя каждый байт точкой, например: `192.168.1.1`. Такая запись может быть конвертирована из текста в 32-битный адрес с помощью функций `inet_aton` или `inet_addr`. ## Закрытие сетевого соединения Системный вызов `close` предназначен для закрытия *файлового дескриптора*, и его нужно вызывать для того, чтобы освободить запись в таблице файловых дескрипторов. Это является необходимым, но не достаточным требованием при работе с TCP-сокетами. Помимо закрытия файлового дескриптора, хорошим тоном считается уведомление противоположной стороны о том, что сетевое соединение закрывается Это уведомление осуществляется с помощью системного вызова `shutdown`. ## Использование сокетов в роли сервера Для использования сокета в роли сервера, необходимо выполнить следующие действия: 1. Связать сокет с некоторым адресом. Для этого используется системный вызов `bind`, параметры которого точно такие же, как для системного вызова `connect`. Если на компьютере более одного IP-адреса, то адрес `0.0.0.0` означает "все адреса". Часто при отладке и возникает проблема, что порт с определенным номером уже был занят на предыдущем запуске программы (и, например, не был корректно закрыт). Это решается принудительным повторным использованием адреса: ``` // В релизной сборке такого обычно быть не должно! #ifdef DEBUG int val = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val)); #endif ``` 2. Создать очередь, в которой будут находиться входящие, но ещё не принятые подключения. Это делается с помощью системного вызова `listen`, который принимает в качестве параметра максимальное количество ожидающих подключений. Для Linux это значение равно 128, определено в константе `SOMAXCONN`. 3. Принимать по одному соединению с помощью системного вызова `accept`. Второй и третий параметры этого системного вызова могуть быть `NULL`, если нас не интересует адрес того, кто к нам подключился. Системный вызов `accept` блокирует выполнение до тех пор, пока не появится входящее подключение. После чего - возвращает файловый дескриптор нового сокета, который связан с конкретным клиентом, который к нам подключился. ================================================ FILE: practice/sockets-udp/README.md ================================================ # Сетевой взаимодействие без установки соединения ## Протокол UDP ### Схема взаимодействия TCP/IP После создания сокета типа `SOCK_STREAM`, он должен быть подключен к противоположной стороне с помощью системного вызова `connect`, либо принять входящее подключение с помощью системного выхова `accept`. После этого становится возможным сетевое взаимодействие с использованием операций ввода-вывода. Сетевое взаимодействие по TCP/IP (создание сокета с параметрами `AF_INET` и `SOCK_STREAM`) подразумевает, что ядро операционной системы преобразует непрерывный поток данных в последовательность TCP-сегментов, упакованных в IP-пакеты, и наоборот. ### Сокеты UDP Механизм отправки сообщений по UDP подразумевает передачу данных без предварительной установки соединения. Сокет, ориентированный на отправку UDP-сообщений имеет тип `SOCK_DGRAM` и используется совместно с адресацией IPv4 (`AF_INET`) либо IPv6 (`AF_INET6`). ```c // Создание сокета для работы по UDP/IP int sockfd = socket(AF_INET, SOCK_DGRAM, 0); ``` Как и в случае с TCP, для адресация UDP подразумевает, что помимо IP-адреса хоста необходимо определиться с номером порта, который обслуживает отдельный процесс. ### Системные вызовы для передачи и приема данных без установки соединения ```c // Отправить пакет данных ssize_t sendto(int sockfd, // сокет const void *buf, size_t len, // данные и размер int flags, // дополнительные опции // адрес назначения (и его размер как для bind/connect) const struct sockaddr *dest_addr, socklen_t addrlen); // Получить пакет данных ssize_t recvfrom(int sockfd, // сокет void *buf, size_t len, // данные и размер int flags, // дополнительные опции // адрес отправителя (и размер как для accept) const struct sockaddr *src_addr, socklen_t *addrlen); ``` Cистемный вызов `sendto` предназначен для отправки сообщения. Поскольку предварительно соединение не было установлено, то обязательным является указание адреса назначения: IP-адрес хоста и номер порта. Системный вызов `recvfrom` предназначен для приема сообщения, и является блокирующим до тех пор, пока придет хотя бы одно сообщение UDP. Размер буфера, в который `recvfrom` должен записать данные, должен быть достаточного размера для хранения сообщения, в противном случае данные, которые не влезли в буфер, будут потеряны. Для того, чтобы иметь возможность принимать данные по UDP, необходимо анонсировать прослушивание определенного порта с помощью системного вызова `bind`; параметры адреса для `recvfrom` предназначены только для получения информации об отправителе, и являются опциональными (эти значения могут быть NULL). ## Инструменты в Linux для отладки сетевого взаимодействия ### Сетевой ввод-вывод Команда `nc` (сокращение от `netcat`) работает аналогично команде `cat`, но в качестве аргумента принимает не имя файла для вывода потока данных, а пару `хост порт`. Параметр `-u` означает отправку UDP-пакета. Если предполагается использовать только адресацию IPv4, но не IPv6, то используется опция `-4`. ```bash # Пример: передать данные из in.dat в UDP-сокет на localhost # порт 3000 и записать вывод в файл out.dat > cat in.dat | nc -4 -u localhost 3000 >out.dat ``` ### Режим Бога Утилита `wireshark` позволяют просматривать абсолютно все пакеты на уровне от `Ethernet`, которые проходят через систему. Для этого требуются права `root`, либо настройка `Linux Capabilities` для команды `/usr/bin/dumpcap`, которая является частью `wireshark`: ```bash sudo /usr/sbin/setcap cap_net_raw,cap_net_admin+eip /usr/bin/dumpcap ``` Кроме того, в некоторых дистрибутивах, например Debian/Ubuntu, необходимо, чтобы пользователь входил в группу `wireshark`. Поскольку через систему проходит много сетевых пакетов, то для поиска только интересующих пакетов необходимо настроить фильтр. ### Python Стандартная библиотека Python содержит средства работы с сокетами, которые в точности соответствуют их аналогам для POSIX. Пример отправки UDP-сообщения: ```python from socket import socket, AF_INET, SOCK_DGRAM IP = "127.0.0.1" PORT = 3000 sock = socket(AF_INET, SOCK_DGRAM) # создание UDP-сокета # Соединение не требуется sock.sendto("Hello!\n", (IP, PORT)) # отправка сообщения ``` Прием UDP-сообщений: ```python from socket import socket, AF_INET, SOCK_DGRAM IP = "127.0.0.1" PORT = 3000 MAX_SIZE = 1024 sock = socket(AF_INET, SOCK_DGRAM) # создание UDP-сокета sock.bind((IP, PORT)) # нужно анонсировать порт while True: data, addr = sock.recvfrom(MAX_SIZE) # получить сообщение print("Got {} from {}", data, addr) ``` ## Linux Capabilities В классических UNIX-системах права процессов на выполнение привилегированных действий разграничиваются только на уровне доступа к файлам, либо на уровне "обычный пользователь" - "администратор". В современных ядрах Linux существует ортогональный механизм для предоставления отдельным программам определенных прав, не связанных с доступом к файлам, который называется [capabilities(7)](http://man7.org/linux/man-pages/man7/capabilities.7.html). Отдельному исполняемому файлу можно назначить маску привилегированных разрешений, которые распространяются только на отдельную программу (но не дочерние процессы) с помощью утилиты `setcap` (требуются права root для запуска). Формат вызова: ```bash > sudo setcap CAPABILITIES+FLAGS EXECUTABLE_FILE ``` Здесь `CAPABILITIES` - одно, либо несколько, разделенных запятыми, полномочий. `FLAGS` - это комбинация флагов: * `p` - (Permitted) - полномочие разрешено для исполняемого файла; * `i` - (Inherited) - может наследоваться при вызове `exec`; * `e` - (Effective) - полномочие включено сразу при запуске программы. При этом, установленные атрибуты `capabilities` не сохраняются: * во время модификации файла (например, в результате перекомпиляции); * при копировании или переименовании файла. Таким образом, чтобы иметь возможность создавать и отлаживать программу, требующую дополнительные полномочия, необходимо обеспечить вызов `setcap` на этапе установки или сборки. Так как `capabilities` это атрибуты файлов, то для их работы требуется поддержка со стороны файловой системы. В стандартных для linux файловых системах с этим проблем нет, но если файл находится на примонтированном разделе с неподдерживающей `capabilities` файловой системой, то попытка установки закончится ошибкой `Failed to set capabilities on file './executable' (Operation not supported)`. Также бывает удобным (для отладки) поставить необходимый набор полномочий на отладчик `gdb`; для корректной работы это требует дополнительно установки того же набора полномочий на командный интерпретатор `bash`. ## Взаимодействие на уровне сетевого интерфейса ### Пакетные сокеты Система Linux позволяет взаимодействовать с сетевыми устройствами на низком уровне, используя специальный тип сокетов: пакетные сокеты `AF_PACKET`. Более подробно работа с сокетами на низком уровне рассмотрена в статье [Introduction to RAW Sockets](https://api.semanticscholar.org/CorpusID:136157623) Для создания таких сокетов требуются либо права `root`, либо настройка `cap_net_raw`, в противном случае системный вызов `socket` вернет значение `-1`. При работе с обычными TCP или UDP сокетами, ядро операционной системы полностью абстрагирует пользовательский процесс от дополнительной информации, связанной с доставкой сетевых данных. При работе с пакетными сокетами необходимо самостоятельно реализовывать обработку требуемых заголовков. Существует два уровня абстракции для пакетных сокетов: передача данных, которые заворачиваются в стандартный фрейм Ethernet `(AF_PACKET, SOCK_DGRAM)`, там и полностью произвольный поток данных `(AF_PACKET, SOCK_RAW)`, который может быть использован, например, для отправки широковещательных Ethernet-фреймов. ![](raw-sockets-figure.png) ### Бинарные заголовки сетевых протоколов Для работы с заголовками сетевых протоколов средствами языков Си/C++ можно использовать обычные структуры. Порядок, в котором объявлены поля структуры в тексте программы, является при этом существенным, поскольку он соответствует тому, в каком порядке хранятся данные. Кроме того, необходимо учитывать тот факт, что компиляторы оптимизируют код, выравнивания поля структур в соответствии с особенностями архитектур процессоров, и необходимо явным образом указывать использование "упакованных" структур. **Пример:** заголовок Ethernet-кадра может быть представлен следующим образом. ```c typedef struct { /* MAC-адрес получателя, 6 байт */ uint8_t destination[6]; /* MAC-адрес отправителя, 6 байт */ uint8_t source[6]; /* Тип передаваемого пакета */ uint16_t type; } __attribute__((__packed__)) ethernet_header_t; ``` Кроме того, необходимо помнить о том, что большинство сетевых протоколов подразумевают использование сетевого порядка байт, поэтому нужно использовать функции `htons`, `ntohs`, и др., для того, чтобы правильно представлять целочисленные значения. ### Адресация без IP-адреса У каждого сетевого интерфейса есть имя в системе, например `eth0` или `wlan0`, которое можно посмотреть в выводе команды `ifconfig`, и *порядковый номер* (индекс). У каждого, даже не настроенного, сетевого интерфейса есть свой аппаратный адрес (MAC-адрес), размер которого обычно 6 байт. При адресации через семейство протоколов `AF_PACKET` используется структура `sockaddr_ll`: ```c struct sockaddr_ll { unsigned short sll_family; /* Always AF_PACKET */ unsigned short sll_protocol; /* Physical-layer protocol */ int sll_ifindex; /* Interface number */ unsigned short sll_hatype; /* ARP hardware type */ unsigned char sll_pkttype; /* Packet type */ unsigned char sll_halen; /* Length of address */ unsigned char sll_addr[8]; /* Physical-layer address */ }; ``` Поле `sll_family` должно иметь значение `AF_PACKET` (поскольку необходимо отделять этот тип адресов от других возможных `struct sockaddr`). Для отправки низкоуровневых пакетов определенному устройству с использованием протокола Ethernet, когда используется комбинация `(AF_PACKET, SOCK_DGRAM)`, необходимо заполнять поля: * `sll_protocol` - значение константы из ``, которая определят тип пакета данных (протокол), который содержится внутри Ethernet-фрейма; * `sll_halen` - длина адреса в байтах; для современных реализаций Ethernet это значение равно `6` (константа `ETH_ALEN` из ``) ; * `sll_ifindex` - индекс сетевого устройства; нумерация начинается с `1`, специальное значение `0` может быть использовано только для чтения (признак того, что интересуют данные из любого устройства); * `sll_addr` - значение MAC-адреса. * Все остальные поля заполняются драйвером устройства и должны быть инициализированы нулями. Если используется отправка пакетов без заголовка Ethernet, то есть, используется комбинация `(AF_PACKET, SOCK_RAW)`, то достаточно указать только порядковый индекс сетевого интерфейса `sll_ifindex`. ### Управление устройствами ввода-вывода Для управления файло-подобными устройствами ввода-вывода используется системный вызов `ioctl`, сигнатура которого такая же, как для `fcntl`: первый аргумент - это файловый дескриптор, затем целочисленная команда, а потом возможны аргументы произвольного типа, в зависимости от команды. Набор команд для работы с сетевыми интерфейсами описан в [man 7 netdevice](http://man7.org/linux/man-pages/man7/netdevice.7.html). Многие из них могут быть выполнены только при наличии соответствующих прав (если модифицируют параметры сетевого интерфейса). С помощью GET-команд, отправляемых через системный вызов `ioctl`, можно выяснить индекс устройства по его имени, связанный с ним MAC-адрес, IP-адрес, если устройство настроено, и т. д. ================================================ FILE: practice/stat_fcntl/README.md ================================================ # Свойства файлов ## Сведения о файле ### Структура `stat` С каждым файлом в файловой системе связана метаинформация (status), которая определяется структурой `struct stat`: ``` struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* Inode number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ /* Backward compatibility */ #define st_atime st_atim.tv_sec #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; ``` Метаинформацию о файле получается с помощью команды `stat ИМЯ_ФАЙЛА` или одного из системных вызовов: * `int stat(const char *file_name, struct stat *stat_buffer)` - получение информации о файле по его имени; * `int fstat(int fd, struct stat *stat_buffer)` - то же самое, но для открытого файлового дескриптора; * `int lstat(const char *path_name, struct stat *stat_buffer)` - аналогично `stat`, но в случае, если имя файла указывает на символическую ссылку, то возвращается информация о самой ссылке, а не о файле, на который она ссылается. ### Режим доступа и типы файлов в POSIX В POSIX есть несколько основных типов файлов: * Регулярный файл (`S_IFREG = 0100000`). Занимает место на диске, содержит самые обычные данные. * Каталог (`S_IFDIR = 0040000`). Файл специального типа, который хранит список имен файлов. * Символическая ссылка (`S_IFLNK = 0120000`). Файл, который ссылается на другой файл (в том числе в другом каталоге или даже на другой файловой системе), и с точки зрения фукнций ввода-вывода ничем не отличается от того файла, на который он ссылается. * Блочные (`S_IFBLK = 0060000`) и символьные (`S_IFCHR = 0020000`) устройства. Используются как удобный способ взаимодействия с оборудованием. * Именованные каналы (`S_IFIFO = 0010000`) и сокеты (`S_IFSOCK = 0140000`), предназначенные для межпроцессного взаимодействия. Тип файла закодирован в одном поле структуры с режимом доступа (`rwxrwxrwx`) - целочисленном `.st_mode`. Для выделения отдельных типов файлов применяются поразрядные операции с помощью одного из макросов: `S_ISREG(m)`, `S_ISDIR(m)`, `S_ISCHR(m)`, `S_ISBLK(m)`, `S_ISFIFO(m)`, `S_ISLNK(m)` и `S_ISSOCK(m)`, которые возвращают `0` в качестве false, и произвольное ненулевое значение в качестве true. Для выделения режима доступа, который закодирован в младших битах `.st_mode`, которые можно извлекать, применяя поразрядные операции с помощью констант `S_IWUSR`, `S_IRGRP`, `S_IXOTH` и др. Полный список констант можно посмотреть в `man 7 inode`. ### Доступ к файлу У каждого файла, помимо режима доступа (`rwx` для владельца, группы и остальных) есть два идентификатора, - целых положительных числа: * `.st_uid` - идентификатор пользователя-владельца файла; * `.st_gid` - идентификатор группы-владельца файла. Права доступа "для владельца" применяются при совпадении идентификатора текущего пользователя (можно получить с помощью `getuid()`) с полем `.st_uid`. Аналогично - для группы при совпадении `getgid()` с `.st_gid`. В остальных случаях действуют права доступа "для остальных". Удобным способом определения прав у текущего пользователя является использование системного вызова `access`: ``` int access(const char *path_name, int mode) ``` Этот системный вызов принимает в качестве параметра `mode` поразрядную комбинацию из флагов `R_OK`, `W_OK`, `X_OK` и `F_OK`, - соответственно способность чтения, записи, выполнения файла, и его существование. Возвращает 0 в случае, если перечисленные аттрибуты допустимы для текущего пользователя, и -1 в противном случае. ## Маска создания файлов При создании новых файлов с помощью системного вызова `open` (и всех высокоуровневых функций, которые используют `open`), нужно обязательно указывать режим доступа для вновь созданных файлов. В реальности режим доступа может отличаться от запрошенного: для вновь созданного файла (или каталога) применяется *маска создания файлов* с помощью поразрядной операции "и-не": ``` /* Пусть umask = 0222 */ open("new_file", O_WRONLY|O_CREAT, 0666); // OK /* Создан файл с аттрибутами 0666 & ~0222 = 0444 */ ``` По умолчанию маска создания файлов равна `0000`, то есть не накладывает никаких ограничений. Системный вызов `umask` позволяет задать явным образом новую маску, которая может быть использована для предотвращения случайного создания файлов со слишком слабозащищенными правами доступа. ## Ссылки и файловые дескрипторы Пара значений, хранящихся в полях `.st_dev` и `.st_ino`, позволяют однозначно идентифицировать любой файл в виртуальной файловой системе до её перезагрузки или отмонтирования каких-то частей файловой системы. Поле `.st_nlink` хранит количество имен, связанных с данным именем файла, причем они могут находиться в разных каталогах одной физической файловой системы. Для файла, который существует в файловой системе, `.st_nlink` всегда не меньше `1`, при удалении файла это число становится равным `0`. Но если файл открыт хотя бы одним процессом в системе, то физически он не удаляется (хотя по имени его уже не найти), и доступен по файловому дескриптору до момента закрытия файла. Удалить программным спосособом файл можно с помощью системного вызова `unlink`. Как следует из названия, этот системный вызов уменьшает количество ссылок `.st_nlink`. Для предотвращения *состояния гонки* (race condition), у многих системных вызовов для работы с файлами существуют аналоги, подразумевающие работу с файловыми дескрипторами, а не с именами файлов. Пример состояния гонки: ``` struct stat st; assert(0==stat("my_file.txt", &st)); // OK /* теперь кто-нибудь извне успевает удалить или переименовать файл между моментами этих двух вызовов */ int fd = open("my_file.txt", O_RDONLY); // ERR: файл не найден /* ??? как же так? Только что был здесь, а теперь недоступен */ ``` Если немного переделать этот пример с использованием `fstat`, то проблема решена: ``` int fd = open("my_file.txt", O_RDONLY); if (-1!=fd) { /* Теперь кто-нибудь удаляет файл, или переименовывает его. Но нас это уже не должно волновать: файл существует до того момента, пока мы его не закроем */ struct stat st; fstat(fd, &st); off_t file_size = st.st_size; // размер доступных данных } ``` ## Разные полезные системные вызовы * Добавление нового имени (увеличение ссылок) - `man 2 link` * Удаление (уменьшение ссылок) - `man 2 unlink` * Создание символической ссылки - `man 2 symlink` * Чтение значения символической ссылки - `man 2 readlink` * Создание каталога - `man 2 mkdir` * Удаление пустого каталога - `man 2 rmdir` * Изменение режима доступа - `man 2 chmod` * Перемещение (переименование) файла - `man 2 rename` ## Аттрибуты файловых дескрипторов Системный вызов `fcntl` предназначен для управления открытыми файлыми дескрипторами. ``` int fcntl(int fd, int get_command); int fcntl(int fd, int set_command, void *set_value); ``` Для открытых файлов можно командами `F_GETFL` и `F_SETFL` получать и менять аттрибуты открытия: `O_APPEND`, `O_ASYNC`, `O_NONBLOCK`. В Linux не возможно изменить режим открытия файлов (например поменять `O_RDONLY` на `O_RDRW`), хотя в некоторых UNIX системах это допускается. ## Блокировки файлов Проблема состояния гонки относится не только к возможным изменениям аттрибутов файлов, но и к проблеме *гонки данных* (data race) между различными процессами, которые хотят одновременно обращаться к одним и тем же файлам. В UNIX-подобных системах существует два основных механизма для блокировки файлов. ### Блокировки BSD flock Поддерживается \*BSD и Linux. Системный вызов `flock` имеет сигнатуру: ``` int flock(int fd, int operation); ``` `fd` - открытый файловый дескриптор, возможные операции: * `LOCK_SH` - получить разделяемую блокировку. Разделяемую блокировку на файл могут накладывать разные процессы, не мешая друг другу, когда им требуется только читать данные из файла. * `LOCK_EX` - получить эксклюзивную блокировку. Только один процесс может это сделать. * `LOCK_UN` - разблокировать файла. Блокировка накладывается не на файловые дескрипторы, а на сами файлы, с с которыми они связаны. При попытке получить блокировку (любого типа) к файлу, который кем-то эксклюзивно заблокирован, приведет к приостановке текущего процесса до тех пора, пока блокировка не будет снята. ### Блокировки POSIX file lock Использует команды `F_GETLK` (получить блокировки), `F_SETLK` (установить блокиовку) и `F_SETLKW` (установить блокировку, но сначала дождаться, когда это возможно) системного вызова `fcntl` для управления блокировками. В качестве третьего аргумента `fcntl` передается структура: ``` struct flock { off_t l_start; /* starting offset */ off_t l_len; /* len = 0 means until end of file */ pid_t l_pid; /* lock owner */ short l_type; /* lock type: read/write, etc. */ short l_whence; /* type of l_start */ int l_sysid; /* remote system id or zero for local */ }; ``` В отличии от BSD `flock`, этот способ является более гибким, и позволяет управлять блокировками отдельных частей файла. Пример: ``` struct flock fl; memset(&fl, 0, sizeof(fl)); // Установить блокировку только для чтения (не эксклюзивно) fl.l_type = F_RDLCK; // Блокировка на весь файл fl.l_whence = SEEK_SET; // по аналогии с lseek fl.l_start = 0; // по аналогии с lseek fl.l_len = 0; // 0 - до конца, либо размер области // Установить блокировку для чтения. Если какой-то процесс // уже захватил файл для записи, то ожидание, пока освободится fcntl(fd, F_SETLKW, &fl); // Блокировка на запись с 10 по 15 байты файла fl.l_type = F_WRLCK; fl.l_start = 10; fl.l_len = 5; fcntl(fd, F_SETLK, &fl); // Снять блокировку с этого же диапазона fl.l_type = F_UNLCK; fcntl(fd, F_SETLK, &fl); // При закрытии файла процессом все блокировки недействительны close(fd); ``` ### Блокировка BSD lockf Упрощенным аналогом для блокировки отдельных частей файла является системный вызов `lockf`; для указания начала области блокировки нужно переместить указатель `lseek`. ``` int lockf(int fd, int cmd, off_t len); ``` В отличии от `fcntl`, этот системный вызов подразумевает только эксклюзивную блокировку (для записи). ### Команды flock и lslocks Команда `flock` позоляет установить блокировку на произвольный файл для запуска какой-либо программы, и снимает блокировку при завершении работы дочернего процесса. Команда `lslocks` отображает таблицу файлов с блокировками. ================================================ FILE: practice/time/README.md ================================================ # Время в UNIX ## API для работы со временем ### Текущее время Время в UNIX-системах определяется как количество секунд, прошедшее с 1 января 1970 года, причем часы идут по стандартному гринвичскому времени (GMT) без учета перехода на летнее время (DST - daylight saving time). 32-разрядные системы должны прекратить своё нормальное существование 19 января 2038 года, поскольку будет переполнение знакового целого типа для хранения количества секунд. Функция `time` возвращает количество секунд с начала эпохи. Аргументом функции (в который можно передать `NULL`) является указатель на переменную, куда требуется записать результат. В случае, когда требуется более высокая точность, чем 1 секунда, можно использовать системный вызов `gettimeofday`, который позволяет получить текущее время в виде структуры: ``` struct timeval { time_t tv_sec; // секунды suseconds_t tv_usec; // микросекунды }; ``` В этом случае, несмотря на то, что в структуре определено поле для микросекунд, реальная точность будет составлять порядка 10-20 миллисекунд для Linux. Более высокую точность можно получить с помощью системного вызова `clock_gettime`. ### Разложение времени на составляющие Человеко-представимое время состоит из даты (год, месяц, день) и времени суток (часы, минуты, секунды). Это описывается структурой: ``` struct tm { /* время, разбитое на составляющие */ int tm_sec; /* секунды от начала минуты: [0 -60] */ int tm_min; /* минуты от начала часа: [0 - 59] */ int tm_hour; /* часы от полуночи: [0 - 23] */ int tm_mday; /* дни от начала месяца: [1 - 31] */ int tm_mon; /* месяцы с января: [0 - 11] */ int tm_year; /* годы с 1900 года */ int tm_wday; /* дни с воскресенья: [0 - 6] */ int tm_yday; /* дни от начала года (1 января): [0 - 365] */ int tm_isdst; /* флаг перехода на летнее время: <0, 0, >0 */ }; ``` Для преобразования человеко-читаемого времени в машинное используется функция `mktime`, а в обратную сторону - одной из функций: `gmtime` или `localtime`. ### Daylight Saving Time Во многих странах используется "летнее время", когда стрелки часов переводятся на час назад. История введения/отмены летнего времени, и его периоды хранится в [базе данных IANA](https://data.iana.org/time-zones/tz-link.html). База данных представляет собой набор правил в текстовом виде, которые компилируются в бинарное представление, используемое библиотекой glibc. Наборы файлов с правилами перехода на летнее время для разных регионов хранятся в `/usr/share/zoneinfo/`. Когда значение `tm_isdst` положительное, то применяется летнее время, значение `tm_isdst` - зимнее. В случае, когда значение `tm_isdst` отрицательно, - используются данные из timezone data. ## Reentrant-функции Многие функции POSIX API разрабатывались во времена однопроцессорных систем. Это может приводить к разным неприятным последствиям: ``` struct tm * tm_1 = localtime(NULL); struct tm * tm_2 = localtime(NULL); // opps! *tm_1 changed! ``` Проблема заключается в том, что некоторые функции, например `localtime`, возвращает указатель на структуру-результат, а не скалярное значение. При этом, сами данные структуры не требуется удалять, - они хранятся в `.data`-области библиотеки glibc. Проблема решается введением *повторно входимых (reentrant)* функций, которые в обязательном порядке требуют в качестве одного из аргументов указатель на место в памяти для размещения результата: ``` struct tm tm_1; localtime_r(NULL, &tm_1); struct tm tm_2; localtime_r(NULL, &tm_2); // OK ``` Использование повторно входимых функций является обязательным (но не достаточным) условием при написании многопоточных программ. Некоторые reentrant-функции уже не актуальны в современных версиях glibc для Linux, и помечены как deprecated. Например, реализация `readdir` использует локальное для каждого потока хранение данных. ## Виды часов Время в UNIX системе представляется в виде двух чисел: количества секунд и количества наносекунд, но это не означает, что точность часов сопоставимо с одной наносекундой. В современных компьютерах архитектуры несколько типов часов: * аппаратные часы, которые работают от отдельной батарейки даже при отключения питания; * счетчик в ядре операционной системы, который периодически обновляется отдельным аппаратным таймером; * счетчик тактов процессора. ### Аппаратные часы Аппаратные часы обычно работают на базе стандартного часового кварца, обеспечивающего частоту 32.768КГц, и имеющие точность, сопоставимую с точностью обычных бытовых часов. Эти часы могут хранить дату как в формате UTC (стандарт, принятый в UNIX-системах), так и локальное время (принято в Windows). В Linux аппаратные часы доступны в виде символьного устройства `/dev/rtc`, доступ к которому есть только у пользователя `root`. Это устройство может быть открыто только для чтения, после чего из него можно читать 32-битные значения - информацию о прерываниях. Настройка поведения часов осуществляется с помощью системного вызова `ioctl` и передачей одной из команд, относящихся к [`rtc(4)`](http://man7.org/linux/man-pages/man4/rtc.4.html). Прерывания могут быть: * каждую секунду, если часы настроены на ежесекундное срабатывание `RTC_UIE` * с частотой от 2 до 8192 Гц, причем частота должна быть степенью двойки `RTC_PIE` * срабатывание в определенное время `RTC_AIE`. Ежесекундное прерывание с использованием часов реального времени: ```c #include // системный вызов ioctl #include // константы RTC_* int rtc = open("/dev/rtc", O_RDONLY); if (-1==rtc) { perror("open /dev/rtc"); exit(1); } // только root может открыть ioctl(rtc, RTC_UIE_ON, 0); // включаем прерывания каждую секунду while (1) { int interrupt_mask; // системный вызов read блокируется до следующего прерывания read(rtc, &interrupt_mask, sizeof(interrupt_mask)); puts("Tick"); } ``` Аппаратные часы хранят информацию о текущей дате и текущем времени с точностью до секунды, причем это время может отличаться от системного. Получение времени из аппаратных часов: ```c #include // системный вызов ioctl #include // константы RTC_*, а ещё структура rtc_time int rtc = open("/dev/rtc", O_RDONLY); if (-1 == rtc) { perror("open /dev/rtc"); exit(1); } struct rtc_time t = {}; // чтение текущего времени из аппаратных часов ioctl(rtc, RTC_RD_TIME, &t); printf("RTC time: %02d : %02d : %02d \n", t.tm_hour, t.tm_min, t.tm_sec); ``` Синхронизация системного времени с аппаратными часами осуществляется при загрузке системы и завершении работы (выключении или перезагрузке). Команда `hwclock` позволяет взаимодействовать с часами реального времени, в том числе и для синхронизации. ```bash > hwclock -r # прочитать и вывести время из RTC > hwclock -w # сохранить системное время в RTC (обычно при выключении) > hwclock -s # установить системное время из RTC (при загрузке) ``` По умолчанию подразумевается, что аппаратные часы используют время UTC, но можно если компьютер используется совместно с системой Windows (двойная загрузка), то можно синхронизировать локальное время, для этого используется опция `-l`. Многие дистрибутивы Linux на этапе установки позволяют указать, какое именно время хранится в часах реального времени. ### Системное время и источники времени Отсчет времени начинается с момента загрузки ядра, и хранится в виде целого количества *тиков* (jiffies), продолжительность которых определяется параметром компиляции ядра `CONFIG_HZ`, и может принимать одно из значений: 100, 250, 300 или 1000 Гц. Для текущего ядра это можно выяснить в файле `/boot/config-ВЕРСИЯ_ЯДРА`. Более высокая частота подразумевает большую нагрузку на процессор, но бывает полезна в некоторых применениях, когда требуется повысить отзывчивость системы. Доступные источники времени зависят от архитектуры процессора и конфигурации ядра, узнать в Linux их можно командой: ```bash > cat /sys/devices/system/clocksource/clocksource0/available_clocksource tsc hpet acpi_pm #^ ^ ^ #| | Legacy-драйвер #| Системный таймер высокой точнсти, обычно работает на частоте от 10Мгц #Регистр Time-Step Counter в самом процессоре ``` Текущий способ определения точного времени хранится в `current_clocksource`: ```bash > cat /sys/devices/system/clocksource/clocksource0/current_clocksource tsc ``` Наиболее точным источником времени, в то же время с минимальным временем доступа, - это счетчик тактов в самом процессоре Time-Step Counter, значение которого, для архитектуры x86 можно получить с помощью команды `RDTSC`. Поскольку получение высокоточных значений времени используется при эксплуатации уязвимостей процессоров Meltdown и Spectre, то этот способ может быть принудительно отключен в системе. Для получения текущего времени из источника времени используется системный вызов `clock_gettime`: ```c #include struct timespec { time_t tv_sec; // время в секундах long tv_nsec; // доля времени в наносекундах }; int clock_gettime(clockid_t id, /* out: */ struct timespec *tp); ``` Первый параметр системного вызова - это целочисленное значение, определяющее, какой именно счетчик или таймер нужно использовать. Для большинства UNIX-систем определены таймеры: * `CLOCK_REAL` - значение астрономического времени, где за точку отсчета принимается *начало эпохи* - 1 января 1970 года; * `CLOCK_MONOTONIC` - значение времени с момента загрузки ядра, исключая то время, пока система находилась в спящем режиме; * `CLOCK_PROCESS_CPUTIME_ID` - значение времени, затраченного на выполнение текущего процесса; * `CLOCK_THREAD_CPUTIME_ID` - значение времени, затраченного на выполнение текущего потока. Этот системный вызов в FreeBSD, и ядре Linux до версии 2.6.21, для стандартных таймеров возвращает значение, которое было обновлено в момент предыдущего аппаратного прерывания от системного таймера, то есть точность времени не превышает продолжительности одного тика. В современных версиях Linux происходит обращение к регистру TSC, либо опрос системного таймера, который возвращает текущее значение с высокой точностью. Часы `CLOCK_REAL_COARSE` и `CLOCK_MONOTONIC_COARSE` возвращают время с точностью до одного тика, как в старых версиях. В системе FreeBSD предусмотрены два вида часов - точные, с суффиксом `_PRECISE`, которые опрашивают системный таймер, и быстрые, с суффиксом `_FAST`, которые возвращают значения с точностью до тика. POSIX-совместимым названиям часов соответствуют `_FAST`-версии. Системный вызов `clock_gettime` реализован в виде [`vdso(7)`](http://man7.org/linux/man-pages/man7/vdso.7.html)-функции, которая доступна в адресном пространстве пользователя. В случае, если происходит опрос часов с низкой точностью (например `CLOCK_REAL_COARSE` в Linux или `CLOCK_REAL_FAST` в FreeBSD), то время вычисляется в адресном пространстве пользователя по значению из счетчика, ранее проставленного планировщиком задач. Если же требуется получить время с высокой точностью и не используется TSC, то может потребоваться настоящий системный вызов для опроса системного таймера. ================================================ FILE: practice/x86-64/README.md ================================================ # Ассемблер архитектуры x86/x86-64 ## Внешние ссылки Основной reference по набору команд [преобразованный в HTML](https://www.felixcloutier.com/x86/). Reference по наборам команд MMX, SSE и AVX [на сайте Intel](https://software.intel.com/sites/landingpage/IntrinsicsGuide/). **Доступ из РФ закрыт, требуется VPN!** То же самое [из неофициального источника](https://www.laruence.com/sse/), доступ пока есть. Неплохой учебник по ассемблеру x86 [на WikiBooks](https://en.wikibooks.org/wiki/X86_Assembly) ## Синтаксис AT&T и Intel Исторически сложилось два синтаксиса языка ассемблера x86: синтаксис AT&T, используемый в UNIX-системах, и синтаксис Intel, используемый в DOS/Windows. Различие, в первую очередь, относится к порядку аргументов команд. Компилятор gcc по умолчанию использует синтаксис AT&T, но с указанием опции `-masm=intel` может переключаться в синтаксис Intel. Кроме того, можно указать используемый синтаксис первой строкой в тексте самой программы: ```nasm .intel_syntax noprefix ``` Здесь параметр `noprefix` после `.intel_syntax` указывает на то, что помимо порядка аргументов, соответствующих синтаксису Intel, ещё и имена регистров не должны начинаться с символа `%`, а константы - с символа `$`, как это принято в синтаксисе AT&T. Мы будем использовать именно этот синтаксис, поскольку с его использованием написано большинство доступной документации и примеров, включая документацию от производителей процессоров. ## Регистры процессора общего назначения Исторически семество процессоров x86 унаследовало набор 8-битных регистров общего назначения семества 8080/8085, которые назывались `a`, `b`, `c` и `d`. Но поскольку процессор 8086 стал 16-битным, то регистры стали назваться `ax`, `bx`, `cx` и `dx`. В 32-битных процессорах они называются `eax`, `ebx`, `ecx` и `edx`, в 64-битных `rax`, `rbx`, `rcx` и `rdx`. Кроме того, в x86 есть регистры "двойного назначения", которые можно использовать, в том числе, в качестве регистров общего назначения, если пользоваться ограниченным подмножеством команд процессора: * `rbp` - верхняя граница стека; * `rsi` - индекс элемента массива, из которого выполняется копирование; * `rdi` - индекс элемента массива, в который выполняется копирование. Регистр `rsp` содержит указатель на нижнюю границу стека, поэтому произвольным образом его использовать не рекомендуется. ### Регистры x86-64 64-разрядные регистры для архитектуры x86-64 именуются начиная с буквы `r`. Помимо регистров `rax`...`rsi`, `rdi` можно использовать регистры общего назначение `r9`...`r15`. Указатель стека хранится в `rsp`, верхняя граница стекового фрейма - в `rbp`. Младшие 32-разрядные части регистров `rax`...`rsi`,`rdi`,`rsp`,`rbp` можно адресовать по именам `eax`...`esi`,`edi`,`esp`,`ebp`. При записи значений по 32-битным именам регистров, старшие 32 разряда обнуляются, что приемлемо для операций над 32-разрядными беззнаковыми значениями. Для работы со знаковыми 32-разрядными значениями, например типом `int`, необходимо предварительно выполнять операции *знакового расширения* с помощью команды `movslq` ## Некоторые инструкции **Для синтаксиса Intel** первым аргументов команды является тот, значение которого будет модифицировано, а вторым - которое остается неизменным. ```nasm add DST, SRC /* DST += SRC */ sub DST, SRC /* DST -= SRC */ inc DST /* ++DST */ dec DST /* --DST */ neg DST /* DST = -DST */ mov DST, SRC /* DST = SRC */ imul SRC /* (eax,edx) = eax * SRC - знаковое */ mul SRC /* (eax,edx) = eax * SRC - беззнаковое */ and DST, SRC /* DST &= SRC */ or DST, SRC /* DST |= SRC */ xor DST, SRC /* DST ^= SRC */ not DST /* DST = ~DST */ cmp DST, SRC /* DST - SRC, результат не сохраняется, */ test DST, SRC /* DST & SRC, результат не сохраняется */ adc DST, SRC /* DST += SRC + CF */ sbb DST, SRC /* DST -= SRC - CF */ ``` **Для синтаксиса AT&T** порядок аргументов - противоположный, то есть команда `add %rax, %rbx` вычислит сумму `%rax` и `%rbx`, после чего сохранит результат в регистр `%rbx`, который указан вторым аргументом. ## Флаги процессора В отличии от процессоров ARM, где обновление регистра флагов производится только при наличии специального флага в команде, обозначаемого суффиксом `s`, в процессорах Intel флаги обновляются всегда большинстом инструкций. Флаг `ZF` устанавливается, если в результате операции был получен нуль. Флаг `SF` устанавливается, если в результате операции было получено отрицательное число. Флаг `CF` устанавливается, если в результате выполнения операции произошел перенос из старшего бита результата. Например, для сложения `CF` устанавливается если результат сложения двух беззнаковых чисел не может быть представлен 64-битным беззнаковым числом. Флаг `OF` устанавливается, если в результате выполняния операции произошло переполнение знакового результата. Например, при сложении `OF` устанавливается, если результат сложения двух знаковых чисел не может быть представлен 64-битным знаковым числом. Обратите внимание, что и сложение `add`, и вычитание `sub` устанавливают одновременно и флаг `CF`, и флаг `OF`. Сложение и вычитание знаковых и беззнаковых чисел выполняется совершенно одинаково, и поэтому используется одна инструкция и для знаковой, и для беззнаковой операции. Инструкции `test` и `cmp` не сохраняют результат, а только меняют флаги. ## Управление ходом программы Безусловный переход выполняется с помощью инструкции `jmp` ```nasm jmp label ``` Условные переходы проверяют комбинации арифметических флагов: ```nasm jz label /* переход, если равно (нуль), ZF == 1 */ jnz label /* переход, если не равно (не нуль), ZF == 0 */ jc label /* переход, если CF == 1 */ jnc label /* переход, если CF == 0 */ jo label /* переход, если OF == 1 */ jno label /* переход, если OF == 0 */ jg label /* переход, если больше для знаковых чисел */ jge label /* переход, если >= для знаковых чисел */ jl label /* переход, если < для знаковых чисел */ jle label /* переход, если <= для знаковых чисел */ ja label /* переход, если > для беззнаковых чисел */ jae label /* переход, если >= (беззнаковый) */ jb label /* переход, если < (беззнаковый) */ jbe label /* переход, если <= (беззнаковый) */ ``` Вызов функции и возврат из неё осуществляются командами `call` и `ret` ```nasm call label /* складывает в стек адрес возврата, и переход на label */ ret /* вытаскивает из стека адрес возврата и переходит к нему */ ``` Кроме того, есть составная команда для организации циклов, которая подразумевает, что в регистре `ecx` находится счётчик цикла: ```nasm loop label /* уменьшает значение ecx на 1; если ecx==0, то переход на следующую инструкцию, в противном случае переход на label */ ``` ## Адресация памяти В отличии от RISC-процессоров, x86 позволяет использовать в качестве **один из аргументов** команды как адрес в памяти. **В синтаксисе AT&T** такая адресация записывается в виде: `OFFSET(BASE, INDEX, SCALE)`, где `OFFSET` - это константа, `BASE` и `INDEX` - регистры, а `SCALE` - одно из значений: `1`, `2`, `4` или `8`. Адрес в памяти вычисляется как `OFFSET+BASE+INDEX*SCALE`. Параметры `OFFSET`, `INDEX` и `SCALE` являются опциональными. При их отсутсвтвии подразумевается, что `OFFSET=0`, `INDEX=0`, `SCALE` равен размеру машинного слова. **В синтаксисе Intel** используется более очевидная нотация: `[BASE + INDEX * SCALE + OFFSET]`. ## Соглашения о вызовах для 64-разрядной архитектуры SystemV AMD64 ABI Целочисленные аргументы передаются последовательно в регистрах: `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`. Если передается более 6 аргументов, то оставшиеся - через стек. Вещественные аргументы передаются через регистры `xmm0`...`xmm7`. Возвращаемое значение целочисленного типа должно быть сохранено в `rax`, вещественного - в `xmm0`. Вызываемая функция обязана сохранять на стеке значения регистров общего назначения `rbx`, `rbp`, и регистры `r12`...`r15`. Кроме того, при вызове функции для 64-разрядной архитектуры есть дополнительное требование - перед вызовом функции стек должен быть выровнен по границе 16 байт, то есть необходимо уменьшить значение `rsp` таким образом, оно было кратно 16. Если кроме регистров задействуется стек для передачи параметров, то они должны быть прижаты к нижней выровненной границе стека. Для функций гарантируется 128-байтная "красная зона" в стеке ниже регистра `rsp` - область, которая не будет затронута внешним событием, например, обработчиком сигнала. Таким образом, можно задействовать для адресации локальных переменных память до `rsp-128`. ================================================ FILE: projects/README.md ================================================ # Семестровые проекты ## Общие требования 1. Проект должен быть реализован на языке Си или C++ 2. В поставке проекта должны быть проектные файлы для его сборки с помощью `cmake`. Все использованные сторонние библиотеки должны быть надлежащим образом оформлены в качестве зависимостей, проверяемых при конфигурировании проекта 3. Проект должен быть опубликован в **приватном** репозитории на GitHub или BitBucket. Доступ должен быть предоставлен лектору курса АКОС и своему семинаристу 4. При выполнении проекта допускается использование ограниченного набора сторонних библиотек и технологий, которые перечислены в описании проектов 5. В репозитории должен быть файл `README.md` с описанием проекта и файлы с документацией, достаточной для сборки, настройки и использования полученной программы. ## Описания проектов * [Компилятор языка Оберон](compiler.md) * [Веб-сервер с поддержкой динамической генерации страниц](httpd.md) * [HTTP-прокси сервер](proxy.md) * [Сетевая игра стрелялка](task_doom.pdf) * [Модельный Шелл](shell.pdf) * [Ассемблерные макросы](assembler_macroces.md) ================================================ FILE: projects/assembler_macroces.md ================================================ # Написание набора ассемблерных макросов Требуется написать набор макросов для gnu assembler в спецификации .intel_syntax noprefix в соответствии со спецификацией [здесь](https://github.com/intel-arch-assembler-course/masm/blob/master/doc/macroces_description_ru.md). При этом примеры [здесь](https://github.com/intel-arch-assembler-course/gnu-assembler/tree/master/examples) должны работать с минимальными переделками. Примеры написаны для Macroassembler (masm) - ассемблер используемый microsoft. Так же полезно написать макросы и переписать примеры под 64-х битный ассемблер. Также былог бы интересно реализовать всё это под arm. ================================================ FILE: projects/compiler.md ================================================ # Компилятор языка Оберон Цель - реализовать полноценный компилятор для языка программирования [Оберон](https://en.wikipedia.org/wiki/Oberon_(programming_language) первой версии (без поддержки объектно-ориентированного программирования). В качестве эталонной реализации можно рассматривать компилятор [XDS Oberon](https://www.excelsior.ru/products/xdsdl). На вход компилятор должен принимать текст программы, на выходе - создавать выполняемый файл для операционной системы Linux. Архитектура процессора (ARM, x86 или x86_64) - остается на усмотрение студента. Формальное описание языка: [http://www.ethoberon.ethz.ch/oreport.html](http://www.ethoberon.ethz.ch/oreport.html) EBNF языка: [http://www.ethoberon.ethz.ch/EBNF.html](http://www.ethoberon.ethz.ch/EBNF.html) # Пререквизиты, необходимые для выполнения проекта 1. Теория формальных языков и трансляций 2. Ассемблер целевого языка программирования 3. Системные вызовы, необходимые для запуска других команд 4. Системные вызовы, необходимые для реализации базовой функциональности стандартной библиотеки # Стек технологий Компилятор должен быть реализован на языке Си или С++. Запрещено использовать: * Библиотеки LLVM * Готовые инструменты для парсинга (`yacc`, `bison` и пр.) Разрешается использовать сторонние библиотеки, реализующие регулярные выражения. # Критерии оценивания * **3 балла.** За этот проект не ставится * **4 балла.** За этот проект не ставится * **5 баллов.** Реализован разбор текстов программ без поддержки структур и массивов и его преобразование в текст программы на ассемблере * **6 баллов.** Реализована поддержка массивов или структур * **7 баллов.** Полная реализация языка программирования * **+1 балл.** Реализована цепочка запуска инструментов `gcc` для создания выполняемого файла * **+1 балл.** Реализована стандартная библиотека * **+1 балл.** Выполняемые программы не связываются со стандартной библиотекой языка Си, и могут работать без неё. ================================================ FILE: projects/httpd.md ================================================ # Веб-сервер с поддержкой динамической генерации страниц Цель - реализовать веб-сервер для протокола `HTTP/1.1`, который умеет не только выдавать статические страницы, но и выполнять произвольные программы, взаимодействуя по протоколу [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface). В качестве эталонной реализации можно рассматривать сервер [Apache](http://httpd.apache.org). Реализованный сервер, помимо реализации основной функциональности, должен предполагать, что он работает без пользовательского интерфейста, то есть должны быть предусмотрены механизмы для управления запущенным сервером. # Пререквизиты, необходимые для выполнения проекта 1. Работа со строками 2. Работа с процессами 3. Работа с сигналами 4. Работа с файловыми дескрипторами 5. Работа с сокетами # Стек технологий Разрешается использовать только стандартную библиотеку Си или C++. # Критерии оценивания * **3 балла.** Реализован сервер, который выдает статические страницы. Должна быть реализована функциональность для конфигурирования, запуска и остановки сервера * **5 баллов.** Сервер умеет выполнять CGI-скрипты и выдавать результат их работы * **8 баллов.** Реализована возможность запускать потенциально опасные скрипты с ограничением их возможностей * **+1 балл.** Реализована передача данных скриптам через POST-запросы * **+1 балл.** Поддерживается запись логов сервера, информативность которой можно настраивать ================================================ FILE: projects/proxy.md ================================================ # HTTP-прокси сервер Цель - реализовать веб-сервер для протокола `HTTP/1.1`, который умеет обслуживать с минимальными задержками 100К одновременных соединений, выдавать статические страницы, и перенаправлять запросы на другие веб-серверы, кэшируя их выдачу. В качестве эталонной реализации можно рассматривать сервер [Nginx](http://nginx.org). Реализованный сервер, помимо реализации основной функциональности, должен предполагать, что он работает без пользовательского интерфейста, то есть должны быть предусмотрены механизмы для управления запущенным сервером. # Пререквизиты, необходимые для выполнения проекта 1. Работа со строками 2. Работа с нитями 3. Работа с сигналами 4. Работа с файловыми дескрипторами 5. Работа с сокетами 6. Мультиплексирование ввода-вывода # Стек технологий Разрешается использовать только стандартную библиотеку Си или C++. # Критерии оценивания * **3 балла.** Реализован сервер, который выдает статические страницы. Должна быть реализована функциональность для конфигурирования, запуска и остановки сервера * **5 баллов.** Сервер умеет перенапралять запросы другим серверам * **8 баллов.** Реализованы кеширование ответов пенераправленных запросов и балансировка нагрузки * **+1 балл.** Корректно используется несколько ядер процессора * **+1 балл.** Поддерживается запись логов сервера, информативность которой можно настраивать ================================================ FILE: projects/shell.tex ================================================ \documentclass[11pt, a4paper]{article} \usepackage[utf8]{inputenc} \usepackage[english,russian]{babel} \usepackage[colorlinks=true,linkcolor=blue]{hyperref} \usepackage{listings} \usepackage{color} \usepackage{verbatim} %\usepackage{tocloft} % it requires package texlive-latex-extra in Debian \usepackage{indentfirst} % % for tocloft % %\renewcommand{\cftsecaftersnum}{.} %\renewcommand{\cftsubsecaftersnum}{.} \renewcommand{\thesection}{\arabic{section}.} \definecolor{mygreen}{rgb}{0,0.6,0} \definecolor{mygray}{rgb}{0.5,0.5,0.5} \definecolor{mymauve}{rgb}{0.58,0,0.82} \lstset{ % backgroundcolor=\color{white}, % choose the background color; you must add \usepackage{color} or \usepackage{xcolor} basicstyle=\footnotesize, % the size of the fonts that are used for the code breakatwhitespace=false, % sets if automatic breaks should only happen at whitespace breaklines=true, % sets automatic line breaking captionpos=b, % sets the caption-position to bottom commentstyle=\color{mygreen}, % comment style deletekeywords={...}, % if you want to delete keywords from the given language escapeinside={\%*}{*)}, % if you want to add LaTeX within your code extendedchars=true, % lets you use non-ASCII characters; for 8-bits encodings only, does not work with UTF-8 frame=single, % adds a frame around the code keepspaces=true, % keeps spaces in text, useful for keeping indentation of code (possibly needs columns=flexible) keywordstyle=\color{blue}, % keyword style language=C, % the language of the code morekeywords={*,...}, % if you want to add more keywords to the set numbers=left, % where to put the line-numbers; possible values are (none, left, right) numbersep=5pt, % how far the line-numbers are from the code numberstyle=\tiny\color{mygray}, % the style that is used for the line-numbers rulecolor=\color{black}, % if not set, the frame-color may be changed on line-breaks within not-black text (e.g. comments (green here)) showspaces=false, % show spaces everywhere adding particular underscores; it overrides 'showstringspaces' showstringspaces=false, % underline spaces within strings only showtabs=false, % show tabs within strings adding particular underscores stepnumber=2, % the step between two line-numbers. If it's 1, each line will be numbered stringstyle=\color{mymauve}, % string literal style tabsize=2, % sets default tabsize to 2 spaces title=\lstname % show the filename of files included with \lstinputlisting; also try caption instead of title } \title{Задание практикума: командная оболочка операционной системы} \author{Сальников А.Н.} \date{} \begin{document} \maketitle \tableofcontents \section{Общее описание} Требуется написать программу на языке \textbf{\textit{Си}}, осуществляющую частичную эмуляцию командной оболочки (shell). Примеры программных оболочек: {\selectlanguage{english} Windows Console (cmd)\cite{cmd}, Windows Power Shell\cite{Power_Shell}, bash\cite{guide-eng}, tcsh\cite{tcsh}, zsh\cite{zsh}.} Командная оболочка представляет собой интерактивную программу. Поток команд берется со стандартного потока ввода. Команды shell-у задаются пользователем через командую строку. Считывать команды нужно в цикле до тех пор, пока на очередной итерации не будет получен конец файла (детектируется при помощи константы EOF или проверкой соответствующих возвращаемых значений функции чтения из файлов/потоков) либо не будет введена команда \textbf{exit}. Перед считыванием нужно вывести на экран приглашение к набору команд. Например приглашение может быть таким: \textit{``vasia''} далее символ \textit{\$} и следующий за ним пробел). Длина считываемой строки ограничена только размером виртуальной памяти доступной процессу. Пример приглашения ввода команд (здесь пользователь уже набрал некторорую команду): {\small \begin{verbatim} vasia$ echo "We found: "; find /home -name \*${USER}\* 2>/dev/null| \ wc -l; echo " files" \end{verbatim} } Завершение shell происходит либо по закрытию стандартного потока ввода, либо в случае выполнения встроенной команды \textbf{exit} (Реализованы должны быть оба варианта). Программа должна корректно обрабатывать все ошибки, выдавая осмысленные информационные сообщения о них в стандартный поток ошибок. В том числе обрабатывать ошибки неправильного синтаксиса командной строки. \section{Описание языка команд shell} Командная оболочка shell выполняет группу работ, где каждая работа отделяется от другой символами перевода строки (`{\textbackslash}n` в языке \textbf{\textit{Си}}) и символом `;'. Внутри работы могут встречаться команды, их аргументы и перенаправления ввода/вывода, которые могут разделятся символами конвейера '|'. В строке могут встречаться следующие конструкции: \begin{itemize} \item текст в кавычках (позволяют вставить пробел в аргумент программы). Кавычки бывают двух видов: двойные и одинарные. Отличие текста в двойных ковычках от текста в одинарных заключается в том, что в двойных кавычках производится подстановка переменных, а в одинарных нет. Например:\newline \verb#echo "I am ${USER}"# напечатает для пользователя c именем <> \newline \verb#I am vasia# \newline \verb#echo 'I am ${USER}'# напечатает \newline \verb#I am ${USER}# \item \textit{экранирование символа}. Осуществляется при помощи символа `\textbackslash'. Символ после обратного слэш не будет иметь <<служебного>> смысла. Например обратный слэш позволяет поставить кавычку внутри кавычек. Последовательность `\textbackslash\textbackslash' позволяет ввести обычный одинарный слэш. Символ `\textbackslash' может так же наоборот означать наличие специального смысла символа в ситуации, когда без этой конструкции особого смысла небыло. Если символ `\textbackslash' стоит в конце строки (следующий символ в файле перевод строки), то это означает, что перевод строки <<съедается>>, а строка на новой строчке на самом деле является продолжением текущей. \item \textit{Коментарии}. Если во входной строке встречается символ \textit{`\#'}, который находится не внутри кавычек одинарных или двойных и не экранированн обратным слэшом, то все символы, стоящие после решётки до конца строки игнорируются. \item \textit{Подстановка значений переменных}. Перед тем как интерпретировать команду необходимо подставить в командную строку значения переменных. Могут быть служебные переменные, а могут быть пользовательские переменные. Данное задание предполагает только подстановку служебных переменных. Значение переменной --- это некотороый текст, который сопоставлен имени пременной. В случае встречи в строке конструкции вида: {\small \begin{verbatim} ${ИМЯ_ПЕРЕМЕННОЙ} \end{verbatim} } вместо этой конструкции, в строку, будет подставлена строка, являющаяся значением переменной. \end{itemize} \section{Команды и запуск команд} Команды можно разделить на 2 группы. Первая -- встроенные команды shell. Часто они не требуют запуска отдельных процессов (их реализация является частью кода shell\footnote{Однако они могут присутствовать в конвейере, тогда для них необходимо создавать отдельные процессы, перенаправлять ввод/вывод и т.п., но вместо вызова exec, нужно вызвать функцию в коде shell, реализующую встроенную команду.}), например команда \textbf{cd} или \textbf{pwd}. Внешние команды shell --- это обычные программы, которые запускаются так, что каждая программа оказывается в своём отдельном процессе. На всторенные команды не может быть вызван exec. Команде можно указать список её аргументов. Например: {\small \begin{verbatim} ls -l -a ../.. #конец аргументов \end{verbatim} } передаст программе ls после её запуска: \newline в argv[0] ``ls'', \newline в argv[1] ``-l'', \newline в argv[2] ``-a'', \newline в argv[3] ``../..''. После списка аргументов прогаммы допускеается указывать символы \verb#<# и \verb#>#, которые позволяют считать стандартный ввод из файла или вывести стандартный вывод в файл, а также последовательность символов \verb#>>#, что позволяет дописать вывод программы в конец указанного файла. Например:\newline {\small \begin{verbatim} # Перенаправим ввод команде cat # из файла file.in и допишем # файл file.out cat < file.in >> file.out \end{verbatim} } После перенаправлений ввода/вывода допускается указать символ \verb#&#, который означает запуск команды в фоновом режиме. Подробнее про фоновый режим далее. При помощи символа \verb#|# организуется конвейер. Смысл символа таков, что команда слева от символа делает свой стандартный поток вывода стандартным потоком ввода для команды справа от символа. И так продолжается дальше по цепочке, пока <<вертикальные палки>> не закончатся. В случае, если имело место перенаправление ввода/вывода, то приоритет отдаётся именно перенаправлению, а не конвейеру. В результате роцесс подключённый к конвейеру справа получит себе конец файла, либо процесс слева будет некому <<слушать>> из конвейера и он вероятно получит себе \textit{SIGPIPE}. Символ \verb#&# допускается указывать только в конце определения исполняемой работы shell. То есть до символа задающего конвейер \verb#&# указать нельзя. \section{Режим переднего плана и фоновый режим} В режиме переднего плана (foreground) работы исполняются последовательно одна за другой. Каждая работа на время своего выполнения получает терминал в свой монопольный доступ. После исполнения одной или нескольких работ (в случае, если они разделены точкой с запятой) пользователю выдаётся стандартное приглашение ко вводу следующего набора команд. При этом, нажатие \textit{Ctrl+c} должно приводить к посылке сигнала текущей выполняющейся работе, но не самому процессу реализующему shell. По нажатию \textit{Ctrl+z} работа должна приостанавливаться, после чего выдаётся приглашение shell, которое позволяет запустить новую порцию команд или продолжить выполнение приостановленной ранее работы путём указания её номера во встроенной команде \textbf{fg} или \textbf{bg}. Список имеющихся работ можно посмотреть при помощи команды \textbf{jobs}. Работа запущенная в фоновом режиме (background) не должна обращаться к терминалу. В случае обращения к терминалу процесс из работы запущенной в фоновом режиме должен быть приостановлен (не завершён). Если работа запускается в фоновом режиме, то процессы не должны получать \textit{SIGINT} в случае нажатия \textit{Ctrl+c} на клавиатуре. В случае запуска работы в фоновом режиме shell не вместо ожидания завершения такойц работы, сразу либо выдаёт приглашение ко вводу следующего набора команд, либо выполняет следующую работу (например они были разделены точкой с запятой). В случае завершения фоновой работы shell информирует об этом пользователя, выдавая соответствующее сообщение в стандартный поток ошибок основным процессом shell (тот, который был изначально при запуске приложения). \section{Запуск команд из истории} Шеллы позволяют использовать короткое мнемоническое имя для запуска команды из истории запусков. Для этого используется конструкция вида \verb#!100500#, здесь 100500 - это номер команды в истории команд. Конструкция должна сработать как подстановка переменной, тоесть в то место, где встретилась конструкция подствляется вводившаяся когда-то команда. Дальше могут идти конструкции с точкой-запятой, перенаправление ввода-вывода и т.п. Пример: \begin{verbatim} vasia$ history | grep gcc 1022 gcc -E test_abc.c 1023 gcc -E test_abc.c > /tmp/filo.c 1025 gcc -S test_abc.c 1027 gcc -c test_abc.c 1029 gcc -o test_abc test_abc.c 1032 gcc -o test_abc test_abc.c -lm 1035 gcc -o test_abc test_abc.c -lm 1206 gcc -g redactor.c 1229 gcc -g ilya.c 1476 gcc prog32.c 1852 gcc terminal_manipulation.c 1855 gcc term_mode_change.c 1859 gcc term_mode_change.c 1862 gcc term_mode_change.c 1964 gcc -Wall -ansi -pedantic -g main9.c 1966 gcc -Wall -ansi -pedantic -g main9.c 1968 gcc -Wall -ansi -pedantic -g main9.c 1994 if gcc foo ; then echo "OK"; fi 1995 if gcc foo ; then echo "OK"; else echo "fooo"; fi 2005 history | grep gcc vasia$ !1862 ; !1966 gcc term_mode_change.c ; gcc -Wall -ansi -pedantic -g main9.c \end{verbatim} \section{Дополнительные требования} Требуется реализовать встроенные команды: \begin{itemize} \item \textbf{cd} -- смена текущего каталога \item \textbf{pwd} -- выдача текущего каталога в стандартный поток вывода \item \textbf{jobs}, \textbf{fg}, \textbf{bg} -- выдача списка активных в текущий момент работ \item \textbf{exit} -- выход из shell \item \textbf{history} -- команда показывающая историю команд \item export -- передача переменных окружения в запускаемые процессы из текщего shell \end{itemize} Требуется реализовать как встроенные команды следующие внешние программы: \begin{itemize} \item \textbf{cat} -- с именем \textbf{mcat}, где в качестве параметра либо ничего не указывается, либо указывается имя файла (полный набор параметров реализовывать не нужно). \item \textbf{sed} -- c именем \textbf{msed}. Реализовать минимальный вариант подстановки по образцу, где в качестве шаблона используется просто текст без спецсимволов, которые позволяют задавать синтаксис регулярных выражений \cite{regex}. Первый аргумент задаёт строку образец, второй ааргумент -- строка на которую будет заменён образец. Необходимо осуществлять замену всех вхождений образца в строке. Символ `\textasciicircum' означает, что второй аргумент программы необходимо приписать вначало всех строк. Cимвол `\$' означает, что второй аргумент программы необходимо приписать в конец всех строк. Внутри строк, на которые заменяем может встречаться перевод строки, который задаётся '\\n'. \item \textbf{grep} -- с именем \textbf{mgrep} Реализовать минимальный вариант. Формат шаблона такой же как для предыдущей команды \textbf{msed}. Дополнительно возможны конструкции \textit{.*} -- означает произвольное количесттво любых символов, в том числе пустое; и \textit{.+} -- означает произвольное количество любых символов, но не пустое. \end{itemize} Требуется реализовать подстановку любых переменных, которые перед запуском выставлены в переменных окружения, в том числе служебных: \begin{itemize} \item \verb#$цифра# -- подстановка соответствующего аргумента командной строки самого shell. \item \verb@$#@ -- число параметров переданное shell \item \verb@$?@ -- значение статуса последнего завершившегося процесса в последней выполнившейся работе переднего плана. \item \verb#${USER}# -- login пользователя. \item \verb#${HOME}# -- домашний каталог пользователя. \item \verb#${SHELL}# -- имя shell. Путь до того места в файловой системе, где находится исполняемый файл с ним. (В качестве плохо работающего <<костыля>> допускается реализация в виде подстановки argv[0]). \item \verb#${UID}# -- идентификатор пользователя \item \verb#${PWD}# -- текущий каталог \item \verb#${PID}# -- pid shell \item \verb#$HOSTNAME# -- имя машины на которой запускается shell \end{itemize} %О работе с переменными окружения можно прочитать здесь:~\cite{getenv}. \newpage \section{Рекомендации по написанию кода задания} На начальном этапе при выполнении задания необходимо научиться считывать командные строки шелл и сохранять их в оперативной памяти (не выполняя команды). Рекомендуется данные в памяти хранить как массив структур следующего вида: \begin{lstlisting} struct program { char* name; int number_of_arguments; char** arguments; char *input_file, *output_file; /* NULL - not redirected */ int output_type; /* 1 - rewrite, 2 - append */ }; struct job { int state; /* stopped, background, foreground */ struct program* programs; int number_of_programs; pid_t process_group; pid_t *pids; }; \end{lstlisting} После того, как убедились, что разбор команд происходит правильно можно приступать к реазлизации запуска действий связанных с командами. Для реализации команды \textbf{history} целесообразно использовать очередь с ограничением на её длину. Для корректного отслеживания завершающихся процессов полезно определить действие на приход сигнала \textit{SIGCHLD}. Не стоит надеятся, что служебные переменные, такие как \verb#${HOME}# будут для вас выставлены. Задача Shell как раз заключается в том, что Shell эти переменные определяет сам и выставляет для запускаемых из него программ. \begin{thebibliography}{50} \bibitem{cmd} Официальная документация на команду cmd на сайте Microsft: \url{htps://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/ntcmds.mspx?mfr=true}. \bibitem{Power_Shell} Сайт проекта Microsoft Power Shell: \url{https://msdn.microsoft.com/en-us/powershell}. \bibitem{tcsh} Сайт tcsh: \url{http://www.tcsh.org/Welcome}. \bibitem{zsh} Сайт одного из наиболее полного всем чем только можно шелла: \url{http://zsh.sourceforge.net/Doc/}. \bibitem{man} Справочная страница с описанием реализации shell /bin/bash: ``man bash''. \bibitem{guide-rus} Перевод руководства по bash скриптам: \url{http://www.opennet.ru/docs/RUS/bash\_scripting\_guide}. \bibitem{guide-eng} Более полное руководство по bash \url{http://www.gnu.org/software/bash/manual/bashref.html} \bibitem{regex} Справочная страница по регулярным выражениям: ``man 7 regex'' \bibitem{getenv} Справочная страница по функции getenv ``man getenv'' \end{thebibliography} \end{document} ================================================ FILE: projects/task_doom.tex ================================================ \documentclass[russian,a4paper]{article} \usepackage[russian]{babel} \usepackage[utf8]{inputenc} \usepackage{amsmath} \usepackage{color} \usepackage{verbatim} \usepackage{hyperref} \hypersetup{pdftex, colorlinks=true, linkcolor=blue, citecolor=blue, filecolor=blue, urlcolor=blue, pdftitle=, pdfauthor=Alexey Salnikov, pdfsubject=, pdfkeywords=} \pagestyle{empty} \textwidth=17cm \hoffset=0cm \voffset=0cm \headheight=0cm \topmargin=0cm \oddsidemargin=0cm \title{Задание практикума: сетевая игра, консольный Doom} \author{Алексей Сальников} \date{} \begin{document} \maketitle % % Вариант 1 % \section{Общее описание} Требуется реализовать игровой сервер, игровой клиент, отображатель статистики. Сервер и клиенты должны взаимодействовать через сеть путём установки TCP соединения. При своём запуске сервер читает файл с картой (имя файла указывается в параметрах при запуске). Сервер должен быть реализован как демон в Unix, тем самым вся диагностическая информация должна выдаваться в специальный файл журнала (Можно в syslog). Для имени файла карты и файла журнала в программе должны быть предусмотрены имена по умолчанию, которые будут подставлены если имена файлов не были указаны в командной строке при запуске. Также специальными параметрами указываются номер TCP порта и имя файла, где будет записан pid сервера (это необходимо чтобы можно было посылать сигналы именно этому экземпляру сервера), по умолчанию: /var/run/как\_там\_мой\_сервер\_называется). Также необходимо иметь возможность запустить серевер не как демон, а как обычную программу. Клиент соединятся с сервером (hostname сервера и номер TCP порта сервера указываются клиенту в аргументах main при запуске (если номер порта небыл указан, использовать некоторый номер порта по умолчанию)). Клиент должен отображать игровое пространство, позволять игроку делать ход, выходить из игры по желанию игрока. (В том числе корректно завершаться по нажатию на Ctrl+c и Ctrl+d). %Отображатель статистики, это отдельная программа, которая может %обращаться к серверу с применением IPC и показывать сведения о ведущих-ся %на сервере играх. Игроки на сервере могут выступать в 2-х ролях: в роли создателя команды игроков, в роли участника команды игроков. В начальный момент, перед тем, как начать игру одним из игроков создаётся команда, и он ожидает, пока нужное количество других участников присоединится к этой команде, чтобы начать игру. Именно создатель команды определяет число участников игры и момент начала игры. При подключении клиент может просмотреть список уже имеющихся команд. При подсоединении или создании команды игрок создаёт себе <<имя>> login. Размер имени не может привышать 60 символов. \section{Описание игры} Игра начинается после того, как создатель команды игроков объявил старт игры. Если игра началась, то присоединиться к ней ещё одному игроку нельзя до тех пор, пока она не будет закончена. Создатель команды игроков может по своему желанию в любой момент завершить игру. Создатель не может принимать участия в игре, но зато по запросу может узнать координаты игроков и их уровень здоровья. Игра происходит в прямоугольном лабиринте, представленным как набор точек некоторой матрицы размера MxN. Лабиринт вместе с его размером должны быть заданы в файле карты, при этом сам файл карты должен иметь текстовое представление легко читаемое человеком. Цель игры оказаться выжившим в лабиринте с максимальным уровнем здоровья. Лабиринт состоит из стенок, аптечек/потравлялок, и проходов. По точке, содержащей аптечку/потравлялку можно двигаться, по стенкам и за границами лабиринта двигаться нельзя. Аптечка обладает лечебным/отравляющим эффектом определённой силы. Игрок может съесть аптечку/потравлялку, при этом к его здоровью прибавляется/отнимается численное значение эффекта. По внешнему виду аптечки/потравлялки нельзя ничего сказать о силе её воздействия и о знаке её воздействия. После употребления аптечки соответствующая клетка считается просто проходом (повторно съесть аптечку нельзя). В лабиринте могут встречаться другие игроки, которые всегда занимают одну точку пространства (в стенке игрок не может находиться). Игроку сопоставляется некоторый уровень здоровья. Который с каждым сделанным им ходом уменьшается на определённое значение (указывается в параметрах серверу). C течением времени, если игрок не перемещается, значение здоровья уменьшается, но не так активно, как в случае перемещения. Игрок помещается в одну из точек с координатами (i,j), два игрока не могут одновременно находится в одной и той же точке. В этой точке он может видеть состояние лабиринта на 10 точек в каждом направлении. То есть будет виден прямоугольник с координатами (i-10,j-10,i+10,j+10). Прямоугольник и всё что в нём есть нужно уметь показывать в консоли в текстовом режиме. За один ход можно произвести одну из следующих операций: \begin{itemize} \item съесть аптечку, находящуюся в текущей точке, \item применить боевой заряд, \item переместиться на 1 соседнюю клетку, \item закладывать мину. \end{itemize} Боевой заряд применяется следующим образом. Вычисляется кратчайшее расстояние, до другого игрока по пути через клетки (если игрок оказался за стенкой, то стенки надо обходить). Радиус действия заряда не превышает 10. Интенсивность удара убывает с расстоянием от игрока, который заряд применяет. Игрока, применившего заряд, удар от этого заряда не травмирует. Игроку предоставляется 10 мин, каждой из которых он может заминировать клетку в лабиринте. Мины не отображаются другим игрокам. В случае попадания игрока на мину к нему применяется ущерб эквивалентный применению боевого заряда на соседней клетке. В случае применения заряда есть время необходимое на перезарядку, в случае минирования игрок на определённое время обездвиживается. В начальный момент игроки расставляются серверм на карту случайным образом. Они должны обязательно оказаться в допустимой точке (то есть не на стенке и не на одной клетке с другим игроком). С начала игры объявляется мораторий на применение оружия определённой длительности. Длительность моратория задаётся в файле с картой и измеряется в секундах. Мощность заряда и скорость убывания здоровья при движении может быть задана в файле с картой. Значения действий аптечек и потравлялок так же указываются в файле с картой. \newpage \section{Формат файла скартой} Далее приведён пример файла с картой: \begin{verbatim} Map 10x20 ###################### # # # # # # # # # ##### # # ##### # # # # ######### ### # # ## # # # ######## # ##### # ## ## # # # ## ### ############# # # # # # # # # # # ###################### initial_health = 500 hit_value = 50 recharge_duration = 3 mining_time = 6 stay_health_drop = 1 movement_health_drop = 4 step_standard_delay = 0.1 moratory_duration = 5 items: 1 1 10 20 20 -10 2 3 8 \end{verbatim} В файле карты, собственно карта обрамлена контуром из решёток, что необходимо для наглядности. Координаты в карте нумеруются начиная с единицы. %\section{Статистика и файл журнала} % %при обращении из программы по сбору статистики должен выдаваться список %команд игроков, где для каждой команды выдаётся: %\begin{itemize} %\item статус игры: идёт, окончилась, не начата; %\item дата начала игры, если начата; %\item дата конца игры, если закончилась; %\item победитель, если определён; %\item список игроков, их текущий уровень здоровья и координаты. %\end{itemize} % %В файл журнала нужно сохранять сведения о начавшихся и закончившихся играх, %а так же выводить имена хостов подключившихся клиентов. Если клиен отключился, %с использованием соответствующей команды, отправляемой серверу, то факт отключения клиента, %так же нужно зафиксировать в журнале. %Сервер, в случае достаточности ресурсов должен обеспечивать возможность подключения %до 1000 клиентов одновременно. \end{document}