Как бы вы ответили на вопрос, что такое операционная система?
Скорее всего, вы легко сможете ответить на этот вопрос человеку далекому от IT, но подобный вопрос вам может задать, например, HR - в попытке переманить вас в какой-нибудь Facebook или Google. С одной стороны, ответить на этот вопрос очень легко и в интернете можно найти много определений, но относится ли, например, конфигурация DNS и файл /etc/resolv.conf
к ОС ? Или в какой области памяти работает ОС - в памяти с безграничными возможностями для кода, называемой kernel space, или все же в лимитированной user space?
На эти вопросы даже в этой статье вы не найдете однозначного ответа, и только вам решать, что для вас ОС, а что пользовательские приложения. Но в конце статьи я все же приведу свои аргументы, почему важно знать, как это работает, даже если вы сеньор программист/админ/девопс, проживший без этих знаний 35 лет и весьма успешно запустивший множество проектов.
Немного о мотивации
Когда-то давно я написал статью про файл дескрипторы и спросил у знакомого, что он об этом думает. Я был полон энтузиазма, так как считал и до сих пор считаю ту статью уникальной в просторах интернета. Но знакомый мне ответил, что это все конечно увлекательно, но только ему это нафиг не нужно, так как он успешно работает без "вот этих вот знаний" на крупных проектах уже второй десяток лет. Так вот, эту статью я пишу для тех, кому интересно, как все работает изнутри. Для тех, кто со временем каким-то чудом не потерял энтузиазм и любознательность. Иногда у меня бывает что-то типа выгорания, когда я не могу сделать ни единой задачи и не могу ничего писать, ни статей ни кода. В такие моменты мне периодически помогает одна моя знакомая психолог, если кому интересно узнать, как она это делает, ссылка тут и тут, как минимум просто полюбуетесь на красивую молодую девушку.
Проще - лучше
Представьте, что вы пишете ОС, и может вас даже зовут Линус, какой набор функций вы бы вложили в ОС? Конечно, хочется предоставить пользователю как можно больше функций, но если у вас уже есть две функции, с помощью которых вы можете осуществить третью, то нужно ли вам создавать отдельно третью? И вообще, стоит ли вам в ядре описывать функционал, который может с точно таким же успехом работать на стороне пользовательского приложения?
Намного безопаснее сохранить код, работающий в привилегированной памяти(kernel space), настолько маленьким, насколько это вообще возможно. Это ускорит разработку ядра, сделает его быстрее и уменьшит количество ошибок и уязвимостей.
Два дружбана
Представьте, что существует совершенно новая ОС, и два разработчика решили написать очень простые приложения для это новой ОС. Один пытается посчитать A+B, второй пытается посчитать A-B. Для примера будем подразумевать язык программирования Си.
На первый взгляд две программы совершенно разные, так как содержат разную логику. Но если просто считать сумму двух констант в коде, то такой код вряд ли будет представлять какую-либо ценность. Ценность будет представлять выполнение определенной функции над неопределенными переменными, а значит эти переменные нужно где-то когда-то определить. Представим, что оба программиста решили ввести данные с клавиатуры, ну а если точнее, с какого-то устройства ввода - это может быть терминал. Также логично предположить, что обоим программистом потребуется вывести куда-то результат. Никому не нужна программа, результат работы которой неизвестен. Таким образом, два программиста пишут не только логику самого приложения, но и кусок кода, отвечающий за ввод данных и за вывод данных с устройства.
Задача становится немного сложнее, если программист не знает или не уверен, с какого устройства пользователь хочет ввести данные.
Итак, два программиста пишут каждый свою программу и с удивлением обнаруживают, что ввод и вывод данных с устройства, так же как и определение нужного устройства, занимают 99% всего кода.
Логичным следующим шагом будет объединение усилий двух программистов и написание общих для обеих программ кусков кода совместными усилиями. Это совершенно не проблема, если программистов всего двое, им легко списаться друг с другом, согласовать детали кода и изменений и тд. Но тут новую ОС решают использовать еще десяток-другой тысяч программистов, и конечно же всем и каждому необходимо решать одни и те же проблемы: определить устройство ввода и устройство вывода, а затем принять и вывести данные. Тысячи и тысячи человек пытаются решить одни и те же проблемы.
Не имеет никакого смысла каждому программисту писать один и тот же код, намного удобнее написать код один раз и использовать его во всех программах всех программистов. Продолжая тему двух программистов, пишущих разные программы для новой ОС, мы получаем примерно следующее
Да программиста встроили общий Си код в свои программы, скомпилировали их и все работает прекрасно. Но вот появился третий программист, который тоже стал использовать этот же код, и который решил его немного улучшить. Но программист попался дотошный и делает изменения в коде почти каждый день. При этом программы, которые уже написаны двумя программистами, требуют перекомпиляции каждый раз, когда кто-то меняет общие участки кода, просто потому что там есть какие-то уязвимости. Никому не понравится перекомпилировать старый код каждый день, если вы не делаете никаких изменений в этом коде от слова совсем.
Поэтому следующим логичным шагом будет отделить общий участок кода от общего скомпилированного файла. Но если ваша программа состоит из двух скомпилированных файлов, в одном из которых находится ваш код, а в другом какой-то общий код, который вы не пишите, а просто используете, то вам этот код нужно каким-то образом загрузить и начать использовать в вашем коде. Это значит, что вы должны написать загрузчик, который загрузит общую библиотеку, и прилинкует ее к вашей программе (передаст нужные указатели на нужные функции).
В этой схеме прекрасно все, никому из программистов больше не нужно перекомпилировать свой код, потому что загрузчик будет подгружать общий код при каждом запуске, и можно расслабиться. Но вот есть одна проблема, сам по себе загрузчик необходим в каждой программе, которая использует общую библиотеку, а это опять приводит к той же проблеме, что и была: каждый программист снова должен писать один и тот же кусок кода, загрузчик и линковщик. Это опять не имеет смысла. Логичнее написать загрузчик один раз и использовать его во всех программах, но так как программисты не хотят перекомпилировать их собственные приложения каждый раз, когда кто-то меняет код в загрузчике, то загрузчик должен быть внешним и не являться частью самой программы и новый подход должен выглядеть примерно так
Давайте немного упростим эту картинку, и переименуем общую библиотеку из "common C library" в "libraryC", а еще лучше просто в "libc". При этом загрузчик (loader) мы вообще сократим до двух букв, просто ld. Эти сокращения крайне важны для понимания аналогии того, что я тут пишу, с ОС linux, которую вы увидите чуть ниже в данной статье.
Итак, мы имеем
Конфигурация
Каждая программа нуждается или может нуждаться в некоторых конфигурационных файлах. Это означает, что каждая отдельно взятая библиотека, или загрузчик, или непосредственно код приложения нуждаются в конфиг файлах.
Напомню, что хоть мы и рассматриваем тут уже 4 совершенно разные программы с разным функционалом, но мы все еще говорим об одном пользовательском приложении, которое запускается в user space и не имеет никакого отношения к ядру ОС.
Теперь о конфигах
В корне файловой системы linux существует директория для хранения конфигурационных файлов, которая называется etc (https://ru.wikipedia.org/wiki/Et_cetera).
Какие именно это конфиги сейчас не так важно, для загрузчика это может быть, например, список директорий, в которых загрузчик будет искать библиотеки. Для общей библиотеки работы с днс libresolv это может быть файл с настройкой днс /etc/resolv.conf, а так же файл /etc/hosts и тд
Проблема запуска
Ну вот теперь-то кажется все проблемы решены. Существует отдельный файл - загрузчик, который может загрузить общий кусок кода, который используют все, назовем его библиотекой, и кусок кода, который непосредственно решает задачу конкретно данной программы или по-другому говоря, логика приложения. Но как же теперь все это дело запустить? Получается, что пользователь ОС не может запустить код программиста напрямую, а должен всегда запускать загрузчик, и уже в качестве аргументов передавать информацию об общeй библиотеке и непосредственно коде приложения. Это очень неудобно. Например, вы хотите выполнить простую команду "ping". Для этого вам нужно запустить ее так
То есть ld libc ping или ld libc ls. Согласитесь, это не очень удобно. Намного удобнее, если пользователь ОС сможет запускать программы просто по их названию, и вместо "ld libc ls" можно будет просто написать "ls". Но если мы запустим просто ls, то запустится код приложения, который просто не будет работать, так как в нем не будет общей части, необходимой для чтения входных данных и вывода данных на экран. Код будет пытаться вызвать функцию, которой не будет в памяти, и адрес для этой функции даже не будет определен.
Эльфы
Как бы это смешно не звучало, но решить эту проблему помогут не гномы и даже не орки, а именно эльфы. Что если мы придумаем формат, в котором мы просто опишем, что в этом файле находится, то есть у нас будут данные, описывающие данные (метаданные), и сами данные, все в одном файле. И назовем мы этот формат ELF (https://en.wikipedia.org/wiki/Executable_and_Linkable_Format).
Так как файл по сути является симбиозом текста и кода, мы можем указать в нем все необходимые данные, а именно, какой загрузчик должен быть запущен, полный путь до файла и т.д. Да это звучит мега странно, вы запускаете файл и указываете в этом файле программу, которая должна этот файл открыть, это звучит как открыть уже открытую бутылку пива. Но если мы договоримся, что ОС будет читать не весь файл, а только, например, начало или какой-то заголовок, и искать там название загрузчика, после чего запускать загрузчик, либо сам файл, если загрузчик не требуется, то получится вполне себе рабочая схема. Таким образом, если мы создадим не просто запускной файл "ls", а запускной файл в формате ELF, то запустив /usr/bin/ls, мы попросим ОС прочитать заголовок из этого файла и запустить другой файл - загрузчик, и передать этот elf файл в качестве аргумента.
То есть запуская /usr/bin/ls, ОС за нас запускает ld /usr/bin/ls. В свою очередь загрузчик должен узнать где-то информацию о библиотеках, необходимых для текущего файла. Логично положить эту информацию в этот же ELF файл. Таким образом, загрузчик при запуске команды ld /usr/bin/ls прочитает этот файл, возьмет список библиотек, и по рекурсии загрузит все необходимые библиотеки после чего запустить непосредственно код приложения.
Немного практики
Начнем разбор возможно с одной из самых часто используемых команд в Linux, команды ls
Смотрим, что это за файл
# file /usr/bin/ls
/usr/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=618c637a7d4bcfd24f3b7017c3198b38b10362e9, stripped
Команда file сама достает название загрузчика из ELF файла, вы можете увидеть это по заголовку interpreter
, но есть множество других путей разобрать ELF файл по частям и увидеть что внутри.
Лично я предпочитаю команды objdump
и ldd
, но все же, если вы хотите понять, что такое ELF формат и что находится внутри ваших исполняемых файлов, вы можете использовать утилиту readelf
Попробуем вывести все заголовки через эту утилиту, и отфильтровать загрузчик
# readelf -a /usr/bin/ls | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
Мы получили путь до файла, который должен быть использован ОС в качестве загрузчика этого ELF файла. То есть каждый раз, когда мы пробуем запустить команду ls
, по факту запускается команда /lib64/ld-linux-x86-64.so.2
, а сам ls
уже идет в качестве аргумента. Смотрим на загрузчик
# ls -lah /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx. 1 root root 10 May 10 2022 /lib64/ld-linux-x86-64.so.2 -> ld-2.28.so
Ссылка на другой файл, проверяем оригинал
# file /lib64/ld-2.28.so
/lib64/ld-2.28.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=67aa0f1504abcfc4befb2b61a329a30a9984e0db, with debug_info, not stripped
Заголовка, указывающего на загрузчик, у этого файла нет, значит он статически слинкован с библиотеками, это означает что все необходимые библиотеки уже находятся внутри этого ELF файла и кода. Это в свою очередь означает, что если вы хотите использовать новую версию библиотеки, например вы обновили glibc библиотеку, то все приложения начнут использовать новую версию при каждом новом запуске, а загрузчик при этом все еще будет использовать старую версию, которая вшита в него. Для обновления libc библиотеки внутри загрузчика вам необходимо перекомпилировать загрузчик, но так как вы сами этого никогда не делаете, то вам нужно просто обновить его версию. Таким образом, обновление libc обычно ведет за собой обновление всех статически слинкованных файлов, таких как загрузчик. Перепроверяем командой ldd
# ldd /lib64/ld-2.28.so
statically linked
В текстовых файлах заголовок interpreter записывается в первой строке в виде загрузчика, который этот файл должен открыть. Следовательно, если вы напишите в вашем python скрипте в первой строке #!/bin/bash вместо #!/bin/python и запустите этот файл, то ваш питон файл будет запущен именно башем, по той же логике, что описана выше.
Теперь пройдемся по всей файловой системе и посмотрим на все исполняемые файлы и какие в них есть загрузчики
root@localhost # find / -type f -executable -exec file '{}' \; | egrep -o "interpreter [a-zA-Z0-9./-]*" | grep ld-linux.so.2
interpreter /lib/klibc-K8e6DOmVI9JpyGMLR7qNe5iZeBk.so
interpreter /lib/ld-linux.so.2
interpreter /lib64/ld-linux-x86-64.so.2
На одной из моих виртуалок я нашел аж 3 разных загрузчика!
Смотрим сколько всего файлов слинкованных файлов
# find / -type f -executable -exec file '{}' \; | grep "dynamically linked" | wc -l
4852
Проверяем теперь сколько содержат в себе указание на какой-либо загрузчик
# find / -type f -executable -exec file '{}' \; | grep "interpreter" | wc -l
4727
Что будет, если мы переименуем загрузчик?
[root@localhost]# mv /lib64/ld-2.28.so /lib64/ld-2.28.so.old
[root@localhost]# ls
-bash: /usr/bin/ls: No such file or directory
[root@localhost]# ps
-bash: /usr/bin/ps: No such file or directory
[root@localhost]# top
-bash: /usr/bin/top: No such file or directory
Отлично, мы все сломали, ничего не работает, все как мы любим. Когда мы запускаем ELF файл, в нем захардкожен путь до файла, и этот файл сейчас имеет другое имя. Но мы можем использовать порядок запуска таким, каким он должен быть
[root@localhost]# /lib64/ld-2.28.so.old /bin/ps
PID TTY TIME CMD
937778 pts/1 00:00:00 sudo
937785 pts/1 00:00:04 bash
985721 pts/1 00:00:00 ld-2.28.so.old
[root@localhost]# /lib64/ld-2.28.so.old /bin/ls
anaconda-ks.cfg bin newls original-ks.cfg
Ну и конечно же мы можем все починить этим же способом
[root@localhost]# /lib64/ld-2.28.so.old /bin/mv /lib64/ld-2.28.so.old /lib64/ld-2.28.so
[root@localhost]# ps
PID TTY TIME CMD
937778 pts/1 00:00:00 sudo
937785 pts/1 00:00:04 bash
986019 pts/1 00:00:00 ps
[root@localhost]# ls
anaconda-ks.cfg bin newls original-ks.cfg
Немного усложним задачу и поменяем права на файл загрузчика, запретив тем самым загрузчику загружаться
[root@localhost]# chmod -x /lib64/ld-2.28.so
[root@localhost]# ls
-bash: /usr/bin/ls: Permission denied
[root@localhost]# top
-bash: /usr/bin/top: Permission denied
[root@localhost]# ps
-bash: /usr/bin/ps: Permission denied
Для тех, кто любит головоломки, я рекомендую попробовать решить эту проблему самостоятельно, иначе продолжаем чтение.
Итак, можно ли починить систему, если у нас все еще осталась открытая рутовая консоль на сервер, но при этом загрузчик совершенно не работает? Код у нас на файловой системе есть, но бит запуска у него не включен, а значит код не исполняемый.
Используем встроенные (built-in) баш команды
[root@localhost]# cd /lib64
[root@localhost lib64]# pwd
/lib64
[root@localhost lib64]# echo *
audit bind bind9-export bpf cifs-utils cracklib_dict.hwm cracklib_dict.pwd cracklib_dict.pwi engines-1.1 eppic_makedumpfile.so fipscheck games gawk gconv gettext gio girepository-1.0 groff gssproxy hmaccalc krb5 ld-2.28.so ld-2.28.so.copy ldb ld-linux-x86-64.so.2 libacl.so.1 libacl.so.1.1.2253 libanl-2.28.so libanl.so.1 libarchive.so.13 libarchive.so.13.3.3 libasm-0.186.so libasm.so.1 libasprintf.so.0 libasprintf.so.0.0.0 libassuan.so.0 libassuan.so.0.8.1 libattr.so.1 libattr.so.1.1.2448 libaudit.so.1 libaudit.so.1.0.0 libauparse.so libauparse.so.0 libauparse.so.0.0.0 libauthselect.so.3 libauthselect.so.3.1.0 libbasicobjects.so.0 libbasicobjects.so.0.1.0 libbfd-2.30-117.el8.so libbind9.so.161 libbind9.so.161.0.4 libblkid.so.1 libblkid.so.1.1.0 libbpf.so.0 libbpf.so.0.4.0 libBrokenLocale-2.28.so libBrokenLocale.so.1 libbrotlicommon.so.1 libbrotlicommon.so.1.0.6 libbrotlidec.so.1 libbrotlidec.so.1.0.6 libbrotlienc.so.1 libbrotlienc.so.1.0.6 libbz2.so.1 libbz2.so.1.0.6 libc-2.28.so libcap-ng.so.0 libcap-ng.so.0.0.0 libcap.so.2 libcap.so.2.48 libcares.so.2 libcares.so.2.2.0 libclamav.so.9 libclamav.so.9.0.5 libclammspack.so.0 libclammspack.so.0.1.0 libcollection.so.4 libcollection.so.4.1.1 libcom_err.so.2 libcom_err.so.2.1 libcomps.so.0 libconfig++.so.9 libconfig.so.9 libconfig++.so.9.2.0 libconfig.so.9.2.0 libcpupower.so.0 libcpupower.so.0.0.1 libcrack.so.2 libcrack.so.2.9.0 libcroco-0.6.so.3 libcroco-0.6.so.3.0.1 libcrypto.so.1.1 libcrypto.so.1.1.1k libcryptsetup.so.12 libcryptsetup.so.12.6.0 libcrypt.so.1 libcrypt.so.1.1.0 libc.so.6 libcurl.so.4 libcurl.so.4.5.0 libdaemon.so.0 libdaemon.so.0.5.0 libdb-5.3.so libdb-5.so libdbus-1.so.3 libdbus-1.so.3.19.7 libdbus-glib-1.so.2 libdbus-glib-1.so.2.3.4 libdebuginfod-0.186.so libdebuginfod.so.1 libdevmapper.so.1.02 libdhash.so.1 libdhash.so.1.1.0 libdhcpctl.so.0 libdhcpctl.so.0.0.0 libdl-2.28.so libdl.so.2 libdnf libdnf.so.2 libdns.so.1115 libdns.so.1115.0.3 libdw-0.186.so libdw.so.1 libe2p.so.2 libe2p.so.2.3 ...
весь вывод я не привожу, очень много файлов в директории
built-in bash - это функции выполняемые внутри уже запущенного процесса bash, а так как bash процесс уже загружен в память и слинкован со всеми библиотеками, то он работает как и работал и уже не зависит от загрузчика. Built-in команды иногда позволяют вам творить чудеса, но об этом лучше написать отдельную статью.
Мы можем посмотреть список файлов в директории, не прибегая к запуску неработающей команды ls, а значит мы можем увидеть список библиотек и найти интуитивно библиотеку, которая не особо значима для ОС и запуска наших приложений, но при этом имеет включенный бит исполнения. Что если мы просто скопируем содержимое одного бинарного файла из загрузчика в другой исполняемый файл? можно ли сделать это с помощью built-in команд? На самом деле файл можно найти и на другом подобном сервере, так как список исполняемых файлов более-менее одинаков. Я выбрал никому ненужный /bin/who и попытаюсь его содержимое переписать содержимым из загрузчика.
while LC_CTYPE=C IFS= read -d '' -r -n 1; do [ "${#REPLY}" == "0" ] && printf '%b' '\0' || printf '%b' "$REPLY"; done < /lib64/ld-2.28.so > /bin/who
К сожалению, я не нашел более быстрого и простого способа копирования бинарных файлов в bash. Надеюсь гуру bash просветят меня и читателей хабра, как сделать это не криворуко, как в моем примере. Эта команда может выполняться несколько минут.
Что же мы теперь имеем? У нас есть исполняемый файл /bin/who, который раньше выводил список залогиненных пользователей, а теперь мы скопировали в него содержимое загрузчика. Можем мы его использовать?
# /bin/who /bin/ls
anaconda-ks.cfg bin newls original-ks.cfg
Да можем. Сможем починить систему?
# /bin/who /bin/chmod +x /lib64/ld-2.28.so
[root@localhost]# ls
anaconda-ks.cfg bin newls original-ks.cfg
Таким образом мы копируем содержимое загрузчика в другой исполняемый файл, который можем легко запустить. После починки оригинального загрузчика можно восстановить оригинальный /bin/who файл просто переустановив пакет
[root@localhost]# rpm -qf /bin/who
coreutils-8.30-13.el8.x86_64
[root@localhost]# yum reinstall coreutils
...
Библиотеки
Список необходимых библиотек находится в том же ELF файле. Посмотреть его можно все той же командой readelf
[root@localhost]# readelf -a /usr/bin/ls | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Так как каждая библиотека является тоже ELF форматом, то любая библиотека может требовать какие-то библиотеки для своих нужд. Поэтому чтобы получить полный список необходимых библиотек, нам нужно запустить readelf на каждую библиотеку рекурсивно.
[root@localhost]# readelf -a /lib/x86_64-linux-gnu/libselinux.so.1 | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libpcre2-8.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
[root@localhost]# readelf -a /lib/x86_64-linux-gnu/libc.so.6 | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
Вы конечно можете написать небольшой скрипт на bash, который рекурсивно проверяет все библиотеки и собирает общий список библиотек, необходимых для каждого исполняемого файла, но в Linux существует уже достаточное количество утилит и для этих целей вы можете использовать команду ldd, которая сделает это за вас
[root@localhost]# ldd /usr/bin/ls
linux-vdso.so.1 (0x00007ffdbf9a4000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f4231d57000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4231b2f000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f4231a98000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4231daf000)
Если кому-то не хватило статьи, тут ссылка на документацию по загрузчику https://man7.org/linux/man-pages/man8/ld.so.8.html
Выводы
На первый взгляд эта тема может показаться немного бесполезной, так как не несет никакого практического значения. Но я считаю своим долгом донести свои доводы, почему это важно знать и понимать.
Первый и вполне очевидный довод - это собеседования. Я очень люблю технические собеседования, и в роли интервьюера и в роли соискателя. Для меня это как кавбойский поединок, где люди мило беседуют на технические темы, и тут знания лишними точно не будут.
Второе - понимание того, как происходит запуск, дает вам знания, что делать если файл загрузчик будет поврежден/удален/переименован. Конечно такое бывает крайне редко, но все же это расширяет спектр проблем, которые вы как инженер можете починить или как минимум понять их суть.
Третий и на мой взгляд более весомый довод - это понимание что такое динамические библиотеки и как с ними работать. Проблемы, когда исполняемый файл не запускается по причине того, что нет необходимой библиотеки или загрузчика бывают часто, и такие команды как ldd помогают решить их. Вот один из примеров странных проблем https://jvns.ca/blog/2021/11/17/debugging-a-weird--file-not-found--error/, но на мой взгляд решение перекомпилировать исполняемый golang файл статически, выбирая при этом alpine докер образ, где glibc библиотека заменена на musl libc, весьма сомнительно. Если вы пишите код, вы должны понимать, что библиотеки, так же как и загрузчик являются неотъемлемой частью вашего кода. Но к сожалению или к счастью, в наши дни некоторые программисты даже framework считают частью ОС.
И наверное самый серьезный аргумент в пользу необходимости понимания как это работает - это осознание того, как изменение конфигурационных файлов влияет на пользовательское приложение. Когда я был еще молод и "умен", я поменял IP адрес ДНС сервера в файле /etc/resolv.conf, и наивно ожидал, что сейчас все мои запросы начнут идти к новому серверу, так как я поменял IP DNS сервера в ОС. И запросы на новый сервер действительно пошли. Но каково же было мое удивление, когда я увидел, что запросы к старому IP почему-то не прекращались. Тогда я еще не понимал, что конфигурация считывается каждым отдельным приложением на момент его загрузки, и только перезапуск процесса помогает перечитать конфигурацию. Виндовый подход мне тогда помог: перезагрузка ОС Linux иногда помогает, но не дает ответы на вопрос, почему оно магическим образом починилось.
Всем добра и мира