WedX - журнал о программировании и компьютерных науках

общая ошибка защиты при запуске os на iso

У меня есть следующий код загрузчика, который отлично работает на жестком диске:

[bits 16]
[org 0x7c00]

bootld_start:
    KERNEL_OFFSET equ 0x2000

    xor ax, ax      ; Explicitly set ES = DS = 0
    mov ds, ax
    mov es, ax
    mov bx, 0x8C00  ; Set SS:SP to 0x8C00:0x0000 . The stack will exist
                    ;     between 0x8C00:0x0000 and 0x8C00:0xFFFF
    mov ss, bx
    mov sp, ax

    mov [BOOT_DRIVE], dl

    mov bx, boot_msg
    call print_string

    mov dl, [BOOT_DRIVE]
    call disk_load

    jmp pm_setup

    jmp $

BOOT_DRIVE:
    db 0

disk_load:
    mov si, dap
    mov ah, 0x42

    int 0x13

    ;cmp al, 4
    ;jne disk_error_132

    ret

dap:
    db 0x10             ; Size of DAP
    db 0
    ; You can only read 46 sectors into memory between 0x2000 and
    ; 0x7C00. Don't read anymore or we overwrite the bootloader we are
    ; executing from. (0x7c00-0x2000)/512 = 46
    dw 46               ; Number of sectors to read
    dw KERNEL_OFFSET    ; Offset
    dw 0                ; Segment
    dd 1
    dd 0

disk_error_132:
    mov bx, disk_error_132_msg
    call print_string

    jmp $

disk_error_132_msg:
    db 'Error! Error! Something is VERY wrong! (0x132)', 0

gdt_start:

gdt_null:
    dd 0x0
    dd 0x0

gdt_code:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10011010b
    db 11001111b
    db 0x0

gdt_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0

gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start
    dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

boot_msg:
    db 'OS is booting files... ', 0

done_msg:
    db 'Done! ', 0

%include "boot/print_string.asm"

pm_setup:
    mov bx, done_msg
    call print_string

    mov ax, 0
    mov ss, ax
    mov sp, 0xFFFC

    mov ax, 0
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    cli
    lgdt[gdt_descriptor]
    mov eax, cr0
    or eax, 0x1
    mov cr0, eax
    jmp CODE_SEG:b32

    [bits 32]

    VIDEO_MEMORY equ 0xb8000
    WHITE_ON_BLACK equ 0x0f

    print32:
        pusha
        mov edx, VIDEO_MEMORY
    .loop:
        mov al, [ebx]
        mov ah, WHITE_ON_BLACK
        cmp al, 0
        je .done
        mov [edx], ax
        add ebx, 1
        add edx, 2
        jmp .loop
    .done:
        popa
        ret

    b32:
        mov ax, DATA_SEG
        mov ds, ax
        mov es, ax
        mov fs, ax
        mov gs, ax
        mov ss, ax

        ; Place stack below EBDA in lower memory
        mov ebp, 0x9c000
        mov esp, ebp

        mov ebx, pmode_msg
        call print32

        call KERNEL_OFFSET

        jmp $

    pmode_msg:
        db 'Protected mode enabled!', 0

kernel:
    mov ebx, pmode_msg
    call print32
    jmp $

pmode_tst:
    db 'Testing...'

times 510-($-$$) db 0
db 0x55
db 0xAA

Проблема в том, что когда я конвертирую его в ISO с помощью этих команд:

mkdir iso
mkdir iso/boot
cp image.flp iso/boot/boot
xorriso -as mkisofs -R -J -c boot/bootcat \
                    -b boot/boot -no-emul-boot -boot-load-size 4 \
                    -o image.iso iso

... он выходит из строя с тройной ошибкой. Когда я запускаю его с qemu-system-i386 -boot d -cdrom os-image.iso -m 512 -d int -no-reboot -no-shutdown, он выводит (за исключением бесполезных исключений SMM):

check_exception old: 0xffffffff new 0xd
     0: v=0d e=0000 i=0 cpl=0 IP=0008:0000000000006616 
pc=0000000000006616 
SP=0010:000000000009bff8 env->regs[R_EAX]=0000000000000000
EAX=00000000 EBX=00007d72 ECX=00000000 EDX=000000e0
ESI=00007cb0 EDI=00000010 EBP=0009c000 ESP=0009bff8
EIP=00006616 EFL=00000083 [--S---C] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0008 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00007c73 00000018
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000         DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=000000e0 CCD=000001b3 CCO=ADDB    
EFER=0000000000000000
check_exception old: 0xd new 0xd
     1: v=08 e=0000 i=0 cpl=0 IP=0008:0000000000006616     pc=0000000000006616 SP=0010:000000000009bff8 env-        >regs[R_EAX]=0000000000000000
EAX=00000000 EBX=00007d72 ECX=00000000 EDX=000000e0
ESI=00007cb0 EDI=00000010 EBP=0009c000 ESP=0009bff8
EIP=00006616 EFL=00000083 [--S---C] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0008 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00007c73 00000018
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000        DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=000000e0 CCD=000001b3 CCO=ADDB    
EFER=0000000000000000
check_exception old: 0x8 new 0xd

Это означает, что я получил 0x0d (общая ошибка защиты), затем 0x08 (двойная ошибка), затем тройная ошибка. Почему это происходит?

РЕДАКТИРОВАТЬ: я изменил команду на:

xorriso -as mkisofs -R -J -c boot/bootcat -b boot/boot.flp -o nmos.iso nmos.flp

Но теперь я получаю следующую ошибку:

xorriso : FAILURE : Cannot find in ISO image: -boot_image ... bin_path='/boot/boot.flp'
xorriso : NOTE : -return_with SORRY 32 triggered by problem severity FAILURE

Кто-нибудь знает что это значит?

РЕДАКТИРОВАТЬ 2:

Я изменил код для чтения с использованием ah=0x02 следующим образом:

mov bx, KERNEL_OFFSET
mov ah, 0x02
mov al, 46
mov ch, 0x00
mov dh, 0x00
mov cl, 0x02
mov dl, [BOOT_DRIVE]

int 0x13

Но это все еще тройная ошибка. Почему?


  • Я не эксперт по ISO, но кажется, что вы используете xorriso для создания ISO, но отключили эмуляцию дискеты. Поскольку ISO не эмулирует дискету, ваше чтение DAP, вероятно, выполняет чтение 2048-байтового сектора, а чтение сектора 1 относится к началу компакт-диска. Вероятно, вы не читаете свое ядро ​​в память, и когда вы переходите к KERNEL_OFFSET (0x2000), вы выполняете память до тех пор, пока она не выйдет из строя по адресу EIP 0x6616. У меня есть подозрение, что ваше ядро ​​просто неправильно читается. 29.05.2017
  • xorriso -as mkisofs -R -J -c boot/bootcat -b boot/boot.flp -o nmos.iso nmos.flp должно быть xorriso -as mkisofs -R -J -c boot/bootcat -b boot/boot.flp -o nmos.iso . Сгенерированный ISO-образ, позволяющий эмуляцию дискеты, называется nmos.iso. Вы можете обнаружить, что это может не работать в BOCHS, QEMU и некоторых реальных аппаратных средствах, поскольку int 13h/ah=42h может быть даже недоступен с CD-ROM, эмулируемым как дискета. 29.05.2017
  • Смотрите последнее предложение моего предыдущего комментария. 29.05.2017
  • Как бы я тогда читал сектора? 29.05.2017
  • Дискеты практически гарантированно поддерживают Int 13h/ah=2 для чтения с диска. 29.05.2017
  • Что касается вашего кода int 13/ah=2h, то большинство BIOS не позволит вам прочитать границу цилиндра/дорожки. Чтение 46 секторов, начиная с сектора 2, головка 0, цилиндр 0, вероятно, приводит к ошибке чтения, и все, что вы читали, не было правильно загружено в память. Другая возможность заключается в том, что ваш образ диска был недостаточно большим, и он пытался прочитать за пределами доступных данных. 30.05.2017
  • Насколько велики цилиндры / гусеницы? 30.05.2017
  • Это зависит от СМИ. Список широко известных форматов гибких дисков для IBM можно найти здесь. Если размер вашего образа диска неизвестен, многие эмуляторы предполагают конфигурацию из 2 головок, 80 дорожек (цилиндров) и 18 секторов на дорожку, что типично для дискеты на 1,44 МБ. 30.05.2017
  • Одно наблюдение. В своем вопросе вы говорите, что ваш загрузчик изначально работал как дискета - я думаю, это было неточно. Глядя на свой проект в github, вы делаете что-то вроде qemu-system-i386 os-image в make-файле. Это фактически заставляет QEMU загружать образ вашего виртуального диска как жесткий диск (а не дискету). Если вы хотите загрузить его как дискету, вы должны сделать это как qemu-system-i386 -fda os-image 30.05.2017

Ответы:


1

Основная причина всех тройных ошибок в вашем вопросе действительно сводится к тому, что ваше ядро ​​​​не загружается должным образом в память по адресу 0x0000: 0x2000. Когда вы передаете управление в это место с помощью JMP, вы в конечном итоге выполняете то, что происходит в области памяти, и ЦП работает до тех пор, пока не встретит инструкцию, вызывающую ошибку.


Загрузочные компакт-диски — это странные звери, у которых есть несколько различных режимов, и существует множество BIOS, которые загружают такие компакт-диски, но у них тоже могут быть свои особенности. Когда вы используете -no-emul-boot с XORRISO, вы запрашиваете, чтобы диск не рассматривался ни как дискета, ни как жесткий диск. Вы можете удалить -no-emul-boot -boot-load-size 4, который должен генерировать ISO, который будет рассматриваться как дискета. Проблема в том, что многие настоящие BIOS, эмуляторы (BOCH и QEMU) и виртуальные машины не поддерживают Int 13h/AH=42h расширенное чтение диска, когда CD загружается с использованием эмуляции гибкого диска. Возможно, вам придется использовать обычное чтение с диска через Int 13h/AH=02h. .

Вы должны иметь возможность использовать расширенное чтение с диска через Int 13h/AH=42h, если вы используете -no-emul-boot -boot-load-size 4, но это потребует некоторых изменений в вашем загрузчике. При использовании компакт-дисков -no-emul-boot -boot-load-size 4 размер сектора составляет 2048 байт, а не 512. Это потребует небольшой модификации вашего загрузчика и ядра. -boot-load-size 4 записывает информацию в ISO, которая информирует BIOS о необходимости чтения 4 фрагментов по 512 байт с начала образа диска внутри ISO. Загрузочная подпись 0xaa55 больше не нужна.

Если вы используете -no-emul-boot, есть еще одна проблема, с которой нужно разобраться. На компакт-диске LBA 0 не находится там, где образ диска помещается в окончательный ISO. Вопрос в том, как вы можете получить LBA, где образ диска находится в ISO? Вы можете заставить XORRISO записать эту информацию в специальный раздел создаваемого вами загрузчика, и вы включите эту функцию с помощью -boot-info-table.

Создать специальный раздел в начале загрузчика относительно просто. В Дополнении к спецификации El Torito упоминается следующее:

EL TORITO BOOT INFORMATION TABLE
...
       The  format of this table is as follows; all integers are in sec-
       tion 7.3.1 ("little endian") format.

         Offset    Name           Size      Meaning
          8        bi_pvd         4 bytes   LBA of primary volume descriptor
         12        bi_file        4 bytes   LBA of boot file
         16        bi_length      4 bytes   Boot file length in bytes
         20        bi_csum        4 bytes   32-bit checksum
         24        bi_reserved    40 bytes  Reserved

       The 32-bit checksum is the sum of all the  32-bit  words  in  the
       boot file starting at byte offset 64.  All linear block addresses
       (LBAs) are given in CD sectors (normally 2048 bytes).

Речь идет о 56 байтах по смещению 8 виртуального диска, который мы создаем с нашим загрузчиком. Если мы изменим верхнюю часть вашего кода загрузчика, чтобы она выглядела так, мы фактически создадим пустую таблицу информации о загрузке:

start:
  jmp bootld_start
  times 8-($-$$) db 0          ; Pad out first 8 bytes

  ; Boot info table
  bi_pvd    dd  0
  bi_file   dd  0
  bi_kength dd  0
  bi_csum   dd  0
  bi_reserved times 40 db 0    ; 40 bytes reserved

При использовании XORRISO с -boot-info-table эта таблица будет заполнена после создания ISO. bi_file — важная часть информации, которая нам понадобится, так как это LBA, где наш образ диска помещается в ISO. Мы можем использовать это, чтобы заполнить пакет доступа к диску, используемый расширенными чтениями с диска для чтения из надлежащего местоположения ISO.

Чтобы сделать DAP немного более читаемым и учесть сектора по 2048 байт, я изменил его, чтобы он выглядел так:

dap:
dap_size:    db 0x10                ; Size of DAP
dap_zero     db 0
    ; You can only read 11 2048 byte sectors into memory between 0x2000 and
    ; 0x7C00. Don't read anymore or we overwrite the bootloader we are
    ; executing from. (0x7c00-0x2000)/2048 = 11 (rounded down)
dap_numsec:  dw 11                  ; Number of sectors to read
dap_offset:  dw KERNEL_OFFSET       ; Offset
dap_segment: dw 0                   ; Segment
dap_lba_low: dd 0
dap_lba_high:dd 0

Одна проблема заключается в том, что LBA помещается в таблицу Boot Information с начала образа диска (сектор с нашим загрузчиком). Нам нужно увеличить этот LBA на 1 и поместить его в DAP, чтобы мы использовали LBA, с которого запускается наше ядро. Используя 32-битную инструкцию, мы можем просто прочитать 32-битное значение из таблицы информации о загрузке, добавить 1 и сохранить его в DAP. При использовании строго 16-битных инструкций добавить единицу к 32-битному значению будет сложнее. Поскольку мы переходим в защищенный режим 386, мы можем предположить, что инструкции с 32-битными операндами поддерживаются в реальном режиме. Код для обновления DAP с помощью LBA ядра может выглядеть так:

    mov ebx, [bi_file]       ; Get LBA of our disk image in ISO
    inc ebx                  ; Add sector to get LBA for start of kernel
    mov [dap_lba_low], ebx   ; Update DAP with LBA of kernel in the ISO

Единственная другая проблема заключается в том, что сектор загрузчика должен быть увеличен до 2048 (размер сектора CD-ROM), а не до 512, и мы можем удалить загрузочную подпись. Сдача:

times 510-($-$$) db 0
db 0x55
db 0xAA

To:

times 2048-($-$$) db 0

Модифицированный код загрузчика может выглядеть так:

[bits 16]
[org 0x7c00]

KERNEL_OFFSET equ 0x2000

start:
  jmp bootld_start
  times 8-($-$$) db 0          ; Pad out first 8 bytes

  ;     Boot info table
  bi_pvd    dd  0
  bi_file   dd  0
  bi_kength dd  0
  bi_csum   dd  0
  bi_reserved times 40 db 0    ; 40 bytes reserved

bootld_start:

        xor ax, ax      ; Explicitly set ES = DS = 0
        mov ds, ax
        mov es, ax
        mov bx, 0x8C00  ; Set SS:SP to 0x8C00:0x0000 . The stack will exist
                        ;     between 0x8C00:0x0000 and 0x8C00:0xFFFF
        mov ss, bx
        mov sp, ax

        mov ebx, [bi_file]       ; Get LBA of our disk image in ISO
        inc ebx                  ; Add sector to get LBA for start of kernel
        mov [dap_lba_low], ebx   ; Update DAP with LBA of kernel in the ISO

        mov [BOOT_DRIVE], dl    
        mov bx, boot_msg
        call print_string

        mov dl, [BOOT_DRIVE]
        call disk_load

        jmp pm_setup

        jmp $

BOOT_DRIVE:
        db 0

disk_load:
        mov si, dap
        mov ah, 0x42

        int 0x13

        ;cmp al, 4
        ;jne disk_error_132

        ret

dap:
dap_size:    db 0x10                ; Size of DAP
dap_zero     db 0
    ; You can only read 11 2048 byte sectors into memory between 0x2000 and
    ; 0x7C00. Don't read anymore or we overwrite the bootloader we are
    ; executing from. (0x7c00-0x2000)/2048 = 11 (rounded down)
dap_numsec:  dw 11                  ; Number of sectors to read
dap_offset:  dw KERNEL_OFFSET       ; Offset
dap_segment: dw 0                   ; Segment
dap_lba_low: dd 0
dap_lba_high:dd 0

disk_error_132:
        mov bx, disk_error_132_msg
        call print_string

        jmp $

disk_error_132_msg:
        db 'Error! Error! Something is VERY wrong! (0x132)', 0

gdt_start:

gdt_null:
    dd 0x0
    dd 0x0

gdt_code:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10011010b
    db 11001111b
    db 0x0

gdt_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0

gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start
    dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

boot_msg:
        db 'OS is booting files... ', 0

done_msg:
        db 'Done! ', 0

%include "boot/print_string.asm"

pm_setup:
        mov bx, done_msg
        call print_string

    mov ax, 0
    mov ss, ax
    mov sp, 0xFFFC

    mov ax, 0
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    cli
    lgdt[gdt_descriptor]
    mov eax, cr0
    or eax, 0x1
    mov cr0, eax
    jmp CODE_SEG:b32

        [bits 32]

        VIDEO_MEMORY equ 0xb8000
        WHITE_ON_BLACK equ 0x0f

        print32:
            pusha
            mov edx, VIDEO_MEMORY
        .loop:
            mov al, [ebx]
            mov ah, WHITE_ON_BLACK
            cmp al, 0
            je .done
            mov [edx], ax
            add ebx, 1
            add edx, 2
            jmp .loop
        .done:
            popa
            ret

        b32:
            mov ax, DATA_SEG
            mov ds, ax
            mov es, ax
            mov fs, ax
            mov gs, ax
            mov ss, ax

        ; Place stack below EBDA in lower memory
            mov ebp, 0x9c000
            mov esp, ebp

            mov ebx, pmode_msg
            call print32

                call KERNEL_OFFSET

            jmp $

        pmode_msg:
                db 'Protected mode enabled!', 0

kernel:
        mov ebx, pmode_msg
        call print32
        jmp $

pmode_tst:
        db 'Testing...'

times 2048-($-$$) db 0

Затем вы можете изменить исходную команду XORRISO следующим образом:

xorriso -as mkisofs -R -J -c boot/bootcat \
                    -b boot/boot -no-emul-boot -boot-load-size 4 \
                    -boot-info-table -o image.iso iso
29.05.2017

2

Я разработчик xorriso. Если image.flp представляет собой образ дискеты с MBR, возможно, таблицей разделов и файловой системой, то намек Майкла идет в правильном направлении. El Torito указывает эмуляции, которые позволяют файлу загрузочного образа отображаться в BIOS как дискета или жесткий диск.

Опция -no-emul-boot -boot-load-size 4 заставляет BIOS загружать первые 2048 байт файла image.flp и выполнять их как программу x86. Очевидно, что образ дискеты не подходит в качестве простой программы.

Согласно традициям mkisofs, по умолчанию используется эмуляция дискет с опцией -b. Таким образом, вам просто нужно удалить опцию -no-emul-boot из командной строки xorriso, чтобы получить загрузочный образ El Torito как дискету. (-boot-load-size 4 также устаревает.) Образ дискеты должен иметь либо 2400, либо 2880, либо 5760 секторов по 512 байт, иначе он будет отклонен xorriso.

Образы других размеров можно эмулировать как жесткие диски, где первая (и единственная) запись раздела в таблице разделов MBR указывает размер диска. xorriso -as mkisofs option -hard-disk-boot выбирает эту эмуляцию.

29.05.2017
  • Единственное, что я могу рассказать о содержимом загрузочного образа и проблемах с BIOS, это то, что git.zytor.com/syslinux/syslinux.git/tree/core/isolinux.asm — хорошо проверенный пример программы загрузки без эмульной загрузки. Мой единственный пример ISO-эмуляции дискеты — программа обновления прошивки Seagate под названием MooseDT-SD1A-2D-8-16-32MB.iso. 29.05.2017
  • El Torito, 4.1 INT 13 Функция 08 дает эмулированную геометрию гибкого диска в шестнадцатеричном формате. Дорожки x головки x секторы: 1,44 Мб = 0x50 x 2 x 0x12, 2,88 Мб = 0x50 x 2 x 0x24, 1,2 Мб = 0x50 x 2 x 0x0F. 30.05.2017
  • Новые материалы

    Объяснение документов 02: BERT
    BERT представил двухступенчатую структуру обучения: предварительное обучение и тонкая настройка. Во время предварительного обучения модель обучается на неразмеченных данных с помощью..

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..

    Учебные заметки: создание моего первого пакета Node.js
    Это мои обучающие заметки, когда я научился создавать свой самый первый пакет Node.js, распространяемый через npm. Оглавление Глоссарий I. Новый пакет 1.1 советы по инициализации..

    Забудьте о Matplotlib: улучшите визуализацию данных с помощью умопомрачительных функций Seaborn!
    Примечание. Эта запись в блоге предполагает базовое знакомство с Python и концепциями анализа данных. Привет, энтузиасты данных! Добро пожаловать в мой блог, где я расскажу о невероятных..


    Для любых предложений по сайту: [email protected]