Rustem Gimadutdinov (rgimad)
73864ff1d7
git-svn-id: svn://kolibrios.org@9019 a494cfbc-eb01-0410-851d-a64ba20cac60
361 lines
32 KiB
Plaintext
361 lines
32 KiB
Plaintext
; Copyright (c) 2008-2009, diamond
|
||
; All rights reserved.
|
||
;
|
||
; Redistribution and use in source and binary forms, with or without
|
||
; modification, are permitted provided that the following conditions are met:
|
||
; * Redistributions of source code must retain the above copyright
|
||
; notice, this list of conditions and the following disclaimer.
|
||
; * Redistributions in binary form must reproduce the above copyright
|
||
; notice, this list of conditions and the following disclaimer in the
|
||
; documentation and/or other materials provided with the distribution.
|
||
; * Neither the name of the <organization> nor the
|
||
; names of its contributors may be used to endorse or promote products
|
||
; derived from this software without specific prior written permission.
|
||
;
|
||
; THIS SOFTWARE IS PROVIDED BY Alexey Teplov aka <Lrz> ''AS IS'' AND ANY
|
||
; EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||
; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||
; DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
|
||
; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||
; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||
; ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||
; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
;*****************************************************************************
|
||
|
||
Встречаются вирус и FAT.
|
||
- Привет, ты кто?
|
||
- Я? Вирус.
|
||
- A я AFT, то есть TAF, то есть FTA, черт, совсем запутался...
|
||
|
||
Бутсектор для FAT12/FAT16-тома на носителе с размером сектора 0x200 = 512 байт.
|
||
|
||
=====================================================================
|
||
|
||
Есть две версии в зависимости от того, поддерживает ли носитель LBA,
|
||
выбор осуществляется установкой константы use_lba в первой строке исходника.
|
||
Требования для работы:
|
||
1) Сам бутсектор, первая копия FAT и все используемые файлы
|
||
должны быть читабельны.
|
||
2) Минимальный процессор - 80186.
|
||
3) В системе должно быть как минимум 592K свободной базовой памяти.
|
||
|
||
=====================================================================
|
||
|
||
Документация в тему (ссылки валидны на момент написания этого файла, 15.05.2008):
|
||
официальная спецификация FAT: http://www.microsoft.com/whdc/system/platform/firmware/fatgen.mspx
|
||
в формате PDF: http://staff.washington.edu/dittrich/misc/fatgen103.pdf
|
||
русский перевод: http://wasm.ru/docs/11/fatgen103-rus.zip
|
||
официальная спецификация расширения EDD BIOS 3.0: http://www.phoenix.com/NR/rdonlyres/19FEBD17-DB40-413C-A0B1-1F3F560E222F/0/specsedd30.pdf
|
||
то же, версия 1.1: http://www.phoenix.com/NR/rdonlyres/9BEDED98-6B3F-4DAC-BBB7-FA89FA5C30F0/0/specsedd11.pdf
|
||
описание функций BIOS: Interrupt List by Ralf Brown: http://www.cs.cmu.edu/~ralf/files.html
|
||
официальная спецификация Boot BIOS: http://www.phoenix.com/NR/rdonlyres/56E38DE2-3E6F-4743-835F-B4A53726ABED/0/specsbbs101.pdf
|
||
|
||
=====================================================================
|
||
|
||
Максимальное количество кластеров на FAT12-томе - 0xFF4 = 4084; каждый кластер
|
||
занимает 12 бит в таблице FAT, так что общий размер не превосходит
|
||
0x17EE = 6126 байт. Вся таблица помещается в памяти.
|
||
Максимальное количество кластеров на FAT16-томе - 0xFFF4 = 65524; каждый
|
||
кластер занимает 16 бит в таблице FAT, так что общий размер не превосходит
|
||
0x1FFE8 = 131048 байт. Вся таблица также помещается в памяти, однако в
|
||
этом случае несколько нецелесообразно считывать всю таблицу, поскольку
|
||
на практике нужна только небольшая её часть. Поэтому место в памяти
|
||
резервируется, но данные считываются только в момент, когда к ним
|
||
действительно идёт обращение.
|
||
|
||
Схема используемой памяти:
|
||
...-7C00 стек
|
||
7C00-7E00 код бутсектора
|
||
7E00-8200 вспомогательный файл загрузчика (kordldr.f1x)
|
||
8200-8300 список загруженных секторов таблицы FAT16
|
||
(1 = соответствующий сектор загружен)
|
||
60000-80000 загруженная таблица FAT12 / место для таблицы FAT16
|
||
80000-90000 текущий кластер текущей рассматриваемой папки
|
||
90000-92000 кэш для корневой папки
|
||
92000-... кэш для некорневых папок (каждой папке отводится
|
||
2000h байт = 100h входов, одновременно в кэше
|
||
может находиться не более 7 папок;
|
||
точный размер определяется размером доступной
|
||
физической памяти - как правило, непосредственно
|
||
перед A0000 размещается EBDA, Extended BIOS Data Area)
|
||
|
||
=====================================================================
|
||
|
||
Основной процесс загрузки.
|
||
Точка входа (start): получает управление от BIOS при загрузке, при этом
|
||
dl содержит идентификатор диска, с которого идёт загрузка
|
||
1. Настраивает стек ss:sp = 0:7C00 (стек располагается непосредственно перед
|
||
кодом), сегмент данных ds = 0, и устанавливает ss:bp на начало
|
||
бутсектора (в дальнейшем данные будут адресоваться через [bp+N] -
|
||
это освобождает ds и экономит на размере кода).
|
||
2. LBA-версия: проверяет, поддерживает ли носитель LBA, вызовом функции 41h
|
||
прерывания 13h. Если нет, переходит на код обработки ошибок с
|
||
сообщением об отсутствии LBA.
|
||
CHS-версия: определяет геометрию носителя вызовом функции 8 прерывания 13h и
|
||
записывает полученные данные поверх BPB. Если вызов завершился ошибкой,
|
||
предполагает уже существующие данные корректными.
|
||
3. Вычисляет некоторые параметры FAT-тома: начальный сектор корневой папки
|
||
и начальный сектор данных. Кладёт их в стек; впоследствии они
|
||
всегда будут лежать в стеке и адресоваться через bp.
|
||
4. Считывает начало корневой папки по адресу 9000:0000. Число считываемых
|
||
секторов - минимум из размера корневой папки, указанного в BPB, и 16
|
||
(размер кэша для корневой папки - 2000h байт = 16 секторов).
|
||
5. Ищет в корневой папке элемент kordldr.f1x. Если не находит, или если
|
||
он оказывается папкой, или если файл имеет нулевую длину -
|
||
переходит на код обработки ошибок с сообщением о
|
||
ненайденном загрузчике.
|
||
Замечание: на этом этапе загрузки искать можно только в корневой
|
||
папке и только имена, заданные в формате файловой системе FAT
|
||
(8+3 - 8 байт на имя, 3 байта на расширение, все буквы должны
|
||
быть заглавными, при необходимости имя и расширение дополняются
|
||
пробелами, разделяющей точки нет, завершающего нуля нет).
|
||
6. Загружает первый кластер файла kordldr.f1x по адресу 0:7E00 и передаёт
|
||
ему управление. При этом в регистрах dx:ax оказывается абсолютный
|
||
номер первого сектора kordldr.f1x, а в cx - число считанных секторов
|
||
(равное размеру кластера).
|
||
|
||
Вспомогательные процедуры бутсектора.
|
||
Код обработки ошибок (err):
|
||
1. Выводит строку с сообщением об ошибке.
|
||
2. Выводит строку "Press any key...".
|
||
3. Ждёт нажатия any key.
|
||
4. Вызывает int 18h, давая шанс BIOSу попытаться загрузиться откуда-нибудь ещё.
|
||
5. Для подстраховки зацикливается.
|
||
|
||
Процедура чтения секторов (read_sectors и read_sectors2):
|
||
на входе должно быть установлено:
|
||
ss:bp = 0:7C00
|
||
es:bx = указатель на начало буфера, куда будут прочитаны данные
|
||
dx:ax = стартовый сектор (относительно начала логического диска
|
||
для read_sectors, относительно начала данных для read_sectors2)
|
||
cx = число секторов (должно быть больше нуля)
|
||
на выходе: es:bx указывает на конец буфера, в который были прочитаны данные
|
||
0. Если вызывается read_sectors2, она переводит указанный ей номер сектора
|
||
в номер относительно начала логического диска, прибавляя номер сектора
|
||
начала данных, хранящийся в стеке как [bp-8].
|
||
1. Переводит стартовый сектор (отсчитываемый от начала тома) в сектор на
|
||
устройстве, прибавляя значение соответствующего поля из BPB.
|
||
2. В цикле (шаги 3-6) читает секторы, следит за тем, чтобы на каждой итерации
|
||
CHS-версия: все читаемые секторы были на одной дорожке.
|
||
LBA-версия: число читаемых секторов не превосходило 7Fh (требование
|
||
спецификации EDD BIOS).
|
||
CHS-версия:
|
||
3. Переводит абсолютный номер сектора в CHS-систему: сектор рассчитывается как
|
||
единица плюс остаток от деления абсолютного номера на число секторов
|
||
на дорожке; дорожка рассчитывается как остаток от деления частного,
|
||
полученного на предыдущем шаге, на число дорожек, а цилиндр - как
|
||
частное от этого же деления. Если число секторов для чтения больше,
|
||
чем число секторов до конца дорожки, уменьшает число секторов для
|
||
чтения.
|
||
4. Формирует данные для вызова int 13h (ah=2 - чтение, al=число секторов,
|
||
dh=головка, (младшие 6 бит cl)=сектор,
|
||
(старшие 2 бита cl и весь ch)=дорожка, dl=диск, es:bx->буфер).
|
||
5. Вызывает BIOS. Если BIOS рапортует об ошибке, выполняет сброс диска
|
||
и повторяет попытку чтения, всего делается не более трёх попыток
|
||
(несколько попыток нужно в случае дискеты для гарантии того, что
|
||
мотор раскрутился). Если все три раза происходит ошибка чтения,
|
||
переходит на код обработки ошибок с сообщением "Read error".
|
||
6. В соответствии с числом прочитанных на текущей итерации секторов
|
||
корректирует текущий сектор, число оставшихся секторов и указатель на
|
||
буфер (в паре es:bx корректируется es). Если всё прочитано, заканчивает
|
||
работу, иначе возвращается на шаг 3.
|
||
LBA-версия:
|
||
3. Если число секторов для чтения больше 7Fh, уменьшает его (для текущей
|
||
итерации) до 7Fh.
|
||
4. Формирует в стеке пакет для int 13h (кладёт все нужные данные командами
|
||
push, причём в обратном порядке: стек - структура LIFO, и данные в
|
||
стеке хранятся в обратном порядке по отношению к тому, как их туда
|
||
клали).
|
||
5. Вызывает BIOS. Если BIOS рапортует об ошибке, переходит на код обработки
|
||
ошибок с сообщением "Read error". Очищает стек от пакета,
|
||
сформированного на предыдущем шаге.
|
||
6. В соответствии с числом прочитанных на текущей итерации секторов
|
||
корректирует текущий сектор, число оставшихся секторов и указатель на
|
||
буфер (в паре es:bx корректируется es). Если всё прочитано, заканчивает
|
||
работу, иначе возвращается на шаг 3.
|
||
|
||
Процедура поиска элемента по имени в уже прочитанных данных папки
|
||
(scan_for_filename):
|
||
на входе должно быть установлено:
|
||
ds:si = указатель на имя файла в формате FAT (11 байт, 8 на имя,
|
||
3 на расширение, все буквы заглавные, если имя/расширение
|
||
короче, оно дополняется до максимума пробелами)
|
||
es = сегмент данных папки
|
||
cx = число элементов в прочитанных данных
|
||
на выходе: ZF определяет, нужно ли продолжать разбор данных папки
|
||
(ZF=1, если либо найден запрошенный элемент, либо достигнут
|
||
конец папки); CF определяет, удалось ли найти элемент с искомым именем
|
||
(CF=1, если не удалось); если удалось, то es:di указывает на него.
|
||
scan_for_filename считает, что данные папки размещаются начиная с es:0.
|
||
Первой командой процедура обнуляет di. Затем просто в цикле по элементам папки
|
||
проверяет имена.
|
||
|
||
Процедура поиска элемента в корневой папке (lookup_in_root_dir):
|
||
на входе должно быть установлено:
|
||
ss:bp = 0:7C00
|
||
ds:si = указатель на имя файла в формате FAT (см. выше)
|
||
на выходе: флаг CF определяет, удалось ли найти файл; если удалось, то
|
||
CF сброшен и es:di указывает на элемент папки
|
||
Начинает с просмотра кэшированной (начальной) части корневой папки. В цикле
|
||
сканирует элементы; если по результатам сканирования обнаруживает,
|
||
что нужно читать папку дальше, то считывает не более 0x10000 = 64K
|
||
байт (ограничение введено по двум причинам: во-первых, чтобы заведомо
|
||
не вылезти за пределы используемой памяти, во-вторых, сканирование
|
||
предполагает, что все обрабатываемые элементы располагаются в одном
|
||
сегменте) и продолжает цикл.
|
||
Сканирование прекращается в трёх случаях: обнаружен искомый элемент;
|
||
кончились элементы в папке (судя по числу элементов, указанному в BPB);
|
||
очередной элемент папки сигнализирует о конце (первый байт нулевой).
|
||
|
||
Процедура вывода на экран ASCIIZ-строки (out_string):
|
||
на входе: ds:si -> строка
|
||
В цикле, пока не достигнут завершающий ноль, вызывает функцию int 10h/ah=0Eh.
|
||
|
||
=====================================================================
|
||
|
||
Работа вспомогательного загрузчика kordldr.f1x:
|
||
1. Определяет, был ли он загружен CHS- или LBA-версией бутсектора.
|
||
В зависимости от этого устанавливает смещения используемых процедур
|
||
бутсектора. Критерий проверки: scan_for_filename должна начинаться
|
||
с инструкции 'xor di,di' с кодом 31 FF (вообще-то эта инструкция может
|
||
с равным успехом ассемблироваться и как 33 FF, но fasm генерирует
|
||
именно такую форму).
|
||
2. Узнаёт размер свободной базовой памяти (т.е. свободного непрерывного куска
|
||
адресов памяти, начинающегося с 0) вызовом int 12h. В соответствии с
|
||
ним вычисляет число элементов в кэше папок. Хотя бы для одного элемента
|
||
место должно быть, отсюда ограничение в 592 Kb (94000h байт).
|
||
Замечание: этот размер не может превосходить 0A0000h байт и
|
||
на практике оказывается немного (на 1-2 килобайта) меньшим из-за
|
||
наличия дополнительной области данных BIOS "вверху" базовой памяти.
|
||
3. Определяет тип файловой системы: FAT12 или FAT16. Согласно официальной
|
||
спецификации от Microsoft (версия 1.03 спецификации датирована,
|
||
к слову, 06 декабря 2000 года), разрядность FAT определяется
|
||
исключительно числом кластеров: максимальное число кластеров на
|
||
FAT12-томе равно 4094 = 0xFF4. Согласно здравому смыслу, на FAT12
|
||
может быть 0xFF5 кластеров, но не больше: кластеры нумеруются с 2,
|
||
а число 0xFF7 не может быть корректным номером кластера.
|
||
Win95/98/Me следует здравому смыслу: разграничение FAT12/16 делается
|
||
по максимуму 0xFF5. Драйвер FAT в WinNT/2k/XP/Vista вообще поступает
|
||
явно неверно, считая, что 0xFF6 (или меньше) кластеров означает
|
||
FAT12-том, в результате получается, что последний кластер
|
||
(в случае 0xFF6) неадресуем. Основной загрузчик osloader.exe
|
||
[встроен в ntldr] для NT/2k/XP делает так же. Первичный загрузчик
|
||
[бутсектор FAT12/16 загружает первый сектор ntldr, и разбор FAT-таблицы
|
||
лежит на нём] в NT/2k подвержен той же ошибке. В XP её таки исправили
|
||
в соответствии со спецификацией. Linux при определении FAT12/FAT16
|
||
честно следует спецификации.
|
||
Здесь код основан всё же на спецификации. 9x мертва, а в линейке NT
|
||
Microsoft если и будет исправлять ошибки, то согласно собственному
|
||
описанию.
|
||
4. Для FAT12: загружает в память первую копию таблицы FAT по адресу 6000:0000.
|
||
Если размер, указанный в BPB, превосходит 12 секторов,
|
||
это означает, что заявленный размер слишком большой (это не считается
|
||
ошибкой файловой системы), и читаются только 12 секторов (таблица FAT12
|
||
заведомо влезает в такой объём данных).
|
||
Для FAT16: инициализирует внутренние данные, указывая, что никакой сектор
|
||
FAT не загружен (они будут подгружаться позднее, когда понадобятся
|
||
и только те, которые понадобятся).
|
||
5. Если кластер равен сектору, то бутсектор загрузил только часть файла
|
||
kordldr.f1x, и загрузчик подгружает вторую свою часть, используя
|
||
значения регистров на входе в kordldr.f1x.
|
||
6. Загружает вторичный загрузчик kord/loader по адресу 1000:0000. Если файл не
|
||
найден, или оказался папкой, или оказался слишком большим, то переходит
|
||
на код обработки ошибок из бутсектора с сообщением
|
||
"Fatal error: cannot load the secondary loader".
|
||
Замечание: на этом этапе имя файла уже можно указывать вместе с путём
|
||
и в формате ASCIIZ, хотя поддержки длинных имён и неанглийских символов
|
||
по-прежнему нет.
|
||
7. Изменяет код обработки ошибок бутсектора на переход на метку hooked_err.
|
||
Это нужно, чтобы последующие обращения к коду бутсектора в случае
|
||
ошибок чтения не выводил соответствующее сообщение с последующей
|
||
перезагрузкой, а рапортовал об ошибке чтения, которую мог бы
|
||
как-нибудь обработать вторичный загрузчик.
|
||
8. Если загрузочный диск имеет идентификатор меньше 0x80,
|
||
то устанавливает al='f' ("floppy"), ah=идентификатор диска,
|
||
иначе al='h' ("hard"), ah=идентификатор диска-0x80 (номер диска).
|
||
Устанавливает bx='12', если тип файловой системы - FAT12, и
|
||
bx='16' в случае FAT16. Устанавливает si=смещение функции обратного
|
||
вызова. Поскольку в этот момент ds=0, то ds:si образуют полный адрес.
|
||
9. Передаёт управление по адресу 1000:0000.
|
||
|
||
Функция обратного вызова для вторичного загрузчика:
|
||
предоставляет возможность чтения файла.
|
||
Вход и выход описаны в спецификации на загрузчик.
|
||
1. Сохраняет стек вызывающего кода и устанавливает свой стек:
|
||
ss:sp = 0:(7C00-8), bp=7C00: пара ss:bp при работе с остальным
|
||
кодом должна указывать на 0:7C00, а -8 берётся от того, что
|
||
инициализирующий код бутсектора уже поместил в стек 2 двойных слова,
|
||
и они должны сохраняться в неизменности.
|
||
2. Разбирает переданные параметры, выясняет, какое действие запрошено,
|
||
и вызывает нужную вспомогательную процедуру.
|
||
3. Восстанавливает стек вызывающего кода и возвращает управление.
|
||
|
||
Вспомогательные процедуры kordldr.f1x.
|
||
Процедура получения следующего кластера в FAT (get_next_cluster):
|
||
1. Вспоминает разрядность FAT, вычисленную ранее.
|
||
Для FAT12:
|
||
2. Устанавливает ds = 0x6000 - сегмент, куда ранее была считана
|
||
вся таблица FAT.
|
||
3. Подсчитывает si = (кластер) + (кластер)/2 - смещение в этом сегменте
|
||
слова, задающего следующий кластер. Загружает слово по этому адресу.
|
||
4. Если кластер имеет нечётный номер, то соответствующий ему элемент
|
||
располагается в старших 12 битах слова, и слово нужно сдвинуть вправо
|
||
на 4 бита; в противном случае - в младших 12 битах, и делать ничего не
|
||
надо.
|
||
5. Выделяет из получившегося слова 12 бит. Сравнивает их с пределом 0xFF7:
|
||
номера нормальных кластеров меньше, и флаг CF устанавливается;
|
||
специальные значения EOF и BadClus сбрасывают флаг CF.
|
||
Для FAT16:
|
||
2. Вычисляет адрес памяти, предназначенной для соответствующего сектора данных
|
||
в таблице FAT.
|
||
3. Если сектор ещё не загружен, то загружает его.
|
||
4. Вычисляет смещение данных для конкретного кластера относительно начала
|
||
сектора.
|
||
5. Загружает слово в ax из адреса, вычисленному на шагах 1 и 3.
|
||
6. Сравнивает его с пределом 0xFFF7: номера нормальных кластеров меньше, и флаг
|
||
CF устанавливается; специальные значения EOF и BadClus сбрасывают CF.
|
||
|
||
Процедура загрузки файла (load_file):
|
||
1. Текущая рассматриваемая папка - корневая. В цикле выполняет шаги 2-4.
|
||
2. Конвертирует имя текущего рассматриваемого компонента имени (компоненты
|
||
разделяются символом '/') в FAT-формат 8+3. Если это невозможно
|
||
(больше 8 символов в имени, больше 3 символов в расширении или
|
||
больше одной точки), возвращается с ошибкой.
|
||
3. Ищет элемент с таким именем в текущей рассматриваемой папке. Для корневой
|
||
папки используется процедура из бутсектора. Для остальных папок:
|
||
a) Проверяет, есть ли такая папка в кэше некорневых папок.
|
||
(Идентификация папок осуществляется по номеру начального кластера.)
|
||
Если такой папки ещё нет, добавляет её в кэш; если тот переполняется,
|
||
выкидывает папку, к которой дольше всего не было обращений. (Для
|
||
каждого элемента кэша хранится метка от 0 до (размер кэша)-1,
|
||
определяющая его номер при сортировке по давности последнего обращения.
|
||
При обращении к какому-то элементу его метка становится нулевой,
|
||
а те метки, которые меньше старого значения, увеличиваются на единицу.)
|
||
б) Просматривает в поисках запрошенного имени все элементы из кэша,
|
||
используя процедуру из бутсектора. Если обнаруживает искомый элемент,
|
||
переходит к шагу 4. Если обнаруживает конец папки, возвращается из
|
||
процедуры с ошибкой.
|
||
в) В цикле считывает папку посекторно. При этом пропускает начальные
|
||
секторы, которые уже находятся в кэше и уже были просмотрены. Каждый
|
||
прочитанный сектор копирует в кэш, если там ещё остаётся место,
|
||
и просматривает в нём все элементы. Работает, пока не случится одно из
|
||
трёх событий: найден искомый элемент; кончились кластеры (судя по
|
||
цепочке кластеров в FAT); очередной элемент папки сигнализирует о конце
|
||
(первый байт нулевой). В двух последних случаях возвращается с ошибкой.
|
||
4. Проверяет тип найденного элемента (файл/папка): последний элемент в
|
||
запрошенном имени должен быть файлом, все промежуточные - папками.
|
||
Если текущий компонент имени - промежуточный, продвигает текущую
|
||
рассматриваемую папку и возвращается к пункту 2.
|
||
5. Проходит по цепочке кластеров в FAT и считывает все кластеры в указанный
|
||
при вызове буфер последовательными вызовами функции бутсектора;
|
||
при этом если несколько кластеров файла расположены на диске
|
||
последовательно, то их чтение объединяется в одну операцию.
|
||
Следит за тем, чтобы не превысить указанный при вызове процедуры
|
||
лимит числа секторов для чтения.
|
||
|
||
Процедура продолжения загрузки файла (continue_load_file): встроена
|
||
внутрь шага 5 load_file; загружает в регистры нужные значения (ранее
|
||
сохранённые из load_file) и продолжает шаг 5.
|