diff --git a/PHILOSOPHY.md b/PHILOSOPHY.md index 4babc183..b521f906 100644 --- a/PHILOSOPHY.md +++ b/PHILOSOPHY.md @@ -21,6 +21,10 @@ way to modify its own EFI executable to bake in the BLAKE2B checksum of the conf a key added to the firmware's keychain. This prevents modifications to the config file (and in turn the checksums contained there) from going unnoticed. +### What about ext2/3/4? Why is that supported then? + +This is explicitly against the philosophy, but it is a pragmatic compromise since a lot of Linux distros and setups expect it to "work that way". It is necessary to keep in mind, additionally, that this is experimental and not explicitly supported, due to lack of maintainance. Maintainers wanted! + ### But I don't want to have a separate FAT boot partition! I don't want it!!! Well tough luck. It is `$year_following_2012` now and most PCs are equipped with UEFI and simply won't boot without a FAT EFI system partition diff --git a/README.md b/README.md index 58319273..5acf6456 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Donations welcome, but absolutely not mandatory! ### Supported filesystems * FAT12/16/32 * ISO9660 (CDs/DVDs) +* ext2/3/4 (NOTE: This is experimental and not supported. Maintainers wanted!) If your filesystem isn't listed here, please read [the philosophy](PHILOSOPHY.md) first, especially before opening issues or pull requests related to this. diff --git a/common/fs/ext2.h b/common/fs/ext2.h new file mode 100644 index 00000000..22f787ac --- /dev/null +++ b/common/fs/ext2.h @@ -0,0 +1,13 @@ +#ifndef __FS__EXT2_H__ +#define __FS__EXT2_H__ + +#include +#include +#include + +bool ext2_get_guid(struct guid *guid, struct volume *part); +char *ext2_get_label(struct volume *part); + +struct file_handle *ext2_open(struct volume *part, const char *path); + +#endif diff --git a/common/fs/ext2.s2.c b/common/fs/ext2.s2.c new file mode 100644 index 00000000..9b56d615 --- /dev/null +++ b/common/fs/ext2.s2.c @@ -0,0 +1,685 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Superblock Fields */ +struct ext2_superblock { + uint32_t s_inodes_count; + uint32_t s_blocks_count; + uint32_t s_r_blocks_count; + uint32_t s_free_blocks_count; + uint32_t s_free_inodes_count; + uint32_t s_first_data_block; + uint32_t s_log_block_size; + uint32_t s_log_frag_size; + uint32_t s_blocks_per_group; + uint32_t s_frags_per_group; + uint32_t s_inodes_per_group; + uint32_t s_mtime; + uint32_t s_wtime; + + uint16_t s_mnt_count; + uint16_t s_max_mnt_count; + uint16_t s_magic; + uint16_t s_state; + uint16_t s_errors; + uint16_t s_minor_rev_level; + + uint32_t s_lastcheck; + uint32_t s_checkinterval; + uint32_t s_creator_os; + uint32_t s_rev_level; + uint16_t s_def_resuid; + uint16_t s_def_gid; + + // if version number >= 1, we have to use the ext2 extended superblock as well + + /* Extended Superblock */ + uint32_t s_first_ino; + + uint16_t s_inode_size; + uint16_t s_block_group_nr; + + uint32_t s_feature_compat; + uint32_t s_feature_incompat; + uint32_t s_feature_ro_compat; + + uint64_t s_uuid[2]; + uint8_t s_volume_name[16]; + + uint64_t s_last_mounted[8]; + + uint32_t compression_info; + uint8_t prealloc_blocks; + uint8_t prealloc_dir_blocks; + uint16_t reserved_gdt_blocks; + uint8_t journal_uuid[16]; + uint32_t journal_inum; + uint32_t journal_dev; + uint32_t last_orphan; + uint32_t hash_seed[4]; + uint8_t def_hash_version; + uint8_t jnl_backup_type; + uint16_t group_desc_size; + uint32_t default_mount_opts; + uint32_t first_meta_bg; + uint32_t mkfs_time; + uint32_t jnl_blocks[17]; +} __attribute__((packed)); + +struct ext2_linux { + uint8_t frag_num; + uint8_t frag_size; + + uint16_t reserved_16; + uint16_t user_id_high; + uint16_t group_id_high; + + uint32_t reserved_32; +} __attribute__((packed)); + +struct ext2_inode { + uint16_t i_mode; + uint16_t i_uid; + + uint32_t i_size; + uint32_t i_atime; + uint32_t i_ctime; + uint32_t i_mtime; + uint32_t i_dtime; + + uint16_t i_gid; + uint16_t i_links_count; + + uint32_t i_blocks_count; + uint32_t i_flags; + uint32_t i_osd1; + uint32_t i_blocks[15]; + uint32_t i_generation; + + /* EXT2 v >= 1.0 */ + uint32_t i_eab; + uint32_t i_maj; + + /* EXT2 vAll */ + uint32_t i_frag_block; + + struct ext2_linux i_osd2; +} __attribute__((packed)); + +struct ext2_file_handle { + struct volume *part; + struct ext2_superblock sb; + int size; + struct ext2_inode root_inode; + struct ext2_inode inode; + uint64_t block_size; + uint32_t *alloc_map; +}; + +/* Inode types */ +#define S_IFIFO 0x1000 +#define S_IFCHR 0x2000 +#define S_IFDIR 0x4000 +#define S_IFBLK 0x6000 +#define S_IFREG 0x8000 +#define S_IFLNK 0xa000 +#define S_IFSOCK 0xc000 + +#define FMT_MASK 0xf000 + +/* EXT2 Filesystem States */ +#define EXT2_FS_UNRECOVERABLE_ERRORS 3 + +/* Ext2 incompatible features */ +#define EXT2_IF_COMPRESSION 0x01 +#define EXT2_IF_EXTENTS 0x40 +#define EXT2_IF_64BIT 0x80 +#define EXT2_IF_INLINE_DATA 0x8000 +#define EXT2_IF_ENCRYPT 0x10000 +#define EXT2_FEATURE_INCOMPAT_META_BG 0x0010 + +/* Ext4 flags */ +#define EXT4_EXTENTS_FLAG 0x80000 + +#define EXT2_S_MAGIC 0xEF53 + +/* EXT2 Block Group Descriptor */ +struct ext2_bgd { + uint32_t bg_block_bitmap; + uint32_t bg_inode_bitmap; + uint32_t bg_inode_table; + + uint16_t bg_free_blocks_count; + uint16_t bg_free_inodes_count; + uint16_t bg_dirs_count; + + uint16_t reserved[7]; +} __attribute__((packed)); + +struct ext4_bgd { + uint32_t bg_block_bitmap; + uint32_t bg_inode_bitmap; + uint32_t bg_inode_table; + + uint16_t bg_free_blocks_count; + uint16_t bg_free_inodes_count; + uint16_t bg_dirs_count; + + uint16_t pad; + uint32_t reserved[3]; + uint32_t block_id_hi; + uint32_t inode_id_hi; + uint32_t inode_table_id_hi; + uint16_t free_blocks_hi; + uint16_t free_inodes_hi; + uint16_t used_dirs_hi; + uint16_t pad2; + uint32_t reserved2[3]; +} __attribute__((packed)); + +/* EXT2 Inode Types */ +#define EXT2_INO_DIRECTORY 0x4000 + +/* EXT2 Directory Entry */ +struct ext2_dir_entry { + uint32_t inode; + uint16_t rec_len; + uint8_t name_len; + uint8_t type; +} __attribute__((packed)); + +struct ext4_extent_header { + uint16_t magic; + uint16_t entries; + uint16_t max; + uint16_t depth; + uint16_t generation; +} __attribute__((packed)); + +struct ext4_extent { + uint32_t block; + uint16_t len; + uint16_t start_hi; + uint32_t start; +} __attribute__((packed)); + +struct ext4_extent_idx { + uint32_t block; + uint32_t leaf; + uint16_t leaf_hi; + uint16_t empty; +} __attribute__((packed)); + +static int inode_read(void *buf, uint64_t loc, uint64_t count, + struct ext2_inode *inode, struct ext2_file_handle *fd, + uint32_t *alloc_map); +static bool ext2_parse_dirent(struct ext2_dir_entry *dir, struct ext2_file_handle *fd, const char *path); + +// parse an inode given the partition base and inode number +static bool ext2_get_inode(struct ext2_inode *ret, + struct ext2_file_handle *fd, uint64_t inode) { + if (inode == 0) + return false; + + struct ext2_superblock *sb = &fd->sb; + + //determine if we need to use 64 bit inode ids + bool bit64 = false; + if (sb->s_rev_level != 0 + && (sb->s_feature_incompat & (EXT2_IF_64BIT)) + && sb->group_desc_size != 0 + && ((sb->group_desc_size & (sb->group_desc_size - 1)) == 0)) { + if(sb->group_desc_size > 32) { + bit64 = true; + } + } + + const uint64_t ino_blk_grp = (inode - 1) / sb->s_inodes_per_group; + const uint64_t ino_tbl_idx = (inode - 1) % sb->s_inodes_per_group; + + const uint64_t block_size = ((uint64_t)1024 << sb->s_log_block_size); + uint64_t ino_offset; + const uint64_t bgd_start_offset = block_size >= 2048 ? block_size : block_size * 2; + const uint64_t ino_size = sb->s_rev_level == 0 ? sizeof(struct ext2_inode) : sb->s_inode_size; + + if (!bit64) { + struct ext2_bgd target_descriptor; + const uint64_t bgd_offset = bgd_start_offset + (sizeof(struct ext2_bgd) * ino_blk_grp); + + volume_read(fd->part, &target_descriptor, bgd_offset, sizeof(struct ext2_bgd)); + + ino_offset = ((target_descriptor.bg_inode_table) * block_size) + + (ino_size * ino_tbl_idx); + } else { + struct ext4_bgd target_descriptor; + const uint64_t bgd_offset = bgd_start_offset + (sizeof(struct ext4_bgd) * ino_blk_grp); + + volume_read(fd->part, &target_descriptor, bgd_offset, sizeof(struct ext4_bgd)); + + ino_offset = ((target_descriptor.bg_inode_table | (bit64 ? ((uint64_t)target_descriptor.inode_id_hi << 32) : 0)) * block_size) + + (ino_size * ino_tbl_idx); + } + + volume_read(fd->part, ret, ino_offset, sizeof(struct ext2_inode)); + + return true; +} + +static uint32_t *create_alloc_map(struct ext2_file_handle *fd, + struct ext2_inode *inode) { + if (inode->i_flags & EXT4_EXTENTS_FLAG) + return NULL; + + size_t entries_per_block = fd->block_size / sizeof(uint32_t); + + // Cache the map of blocks + uint32_t *alloc_map = ext_mem_alloc(inode->i_blocks_count * sizeof(uint32_t)); + for (uint32_t i = 0; i < inode->i_blocks_count; i++) { + uint32_t block = i; + if (block < 12) { + // Direct block + alloc_map[i] = inode->i_blocks[block]; + } else { + // Indirect block + block -= 12; + if (block >= entries_per_block) { + // Double indirect block + block -= entries_per_block; + uint32_t index = block / entries_per_block; + uint32_t indirect_block; + if (index >= entries_per_block) { + uint32_t first_index = index / entries_per_block; + uint32_t first_indirect_block; + volume_read( + fd->part, &first_indirect_block, + inode->i_blocks[14] * fd->block_size + first_index * sizeof(uint32_t), + sizeof(uint32_t) + ); + uint32_t second_index = index % entries_per_block; + volume_read( + fd->part, &indirect_block, + first_indirect_block * fd->block_size + second_index * sizeof(uint32_t), + sizeof(uint32_t) + ); + } else { + volume_read( + fd->part, &indirect_block, + inode->i_blocks[13] * fd->block_size + index * sizeof(uint32_t), + sizeof(uint32_t) + ); + } + for (uint32_t j = 0; j < entries_per_block; j++) { + if (i + j >= inode->i_blocks_count) + return alloc_map; + volume_read( + fd->part, &alloc_map[i + j], + indirect_block * fd->block_size + j * sizeof(uint32_t), + sizeof(uint32_t) + ); + } + i += entries_per_block - 1; + } else { + // Single indirect block + volume_read( + fd->part, &alloc_map[i], + inode->i_blocks[12] * fd->block_size + block * sizeof(uint32_t), + sizeof(uint32_t) + ); + } + } + } + + return alloc_map; +} + +static bool symlink_to_inode(struct ext2_inode *inode, struct ext2_file_handle *fd, + const char *cwd, size_t cwd_len) { + // I cannot find whether this is 0-terminated or not, so I'm gonna take the + // safe route here and assume it is not. + if (inode->i_size < 59) { + struct ext2_dir_entry dir; + char *symlink = (char *)inode->i_blocks; + symlink[59] = 0; + + char *abs = ext_mem_alloc(4096); + char *cwd_copy = ext_mem_alloc(cwd_len + 1); + memcpy(cwd_copy, cwd, cwd_len); + get_absolute_path(abs, symlink, cwd_copy); + + pmm_free(cwd_copy, cwd_len + 1); + + if (!ext2_parse_dirent(&dir, fd, abs)) { + pmm_free(abs, 4096); + return false; + } + pmm_free(abs, 4096); + + ext2_get_inode(inode, fd, dir.inode); + return true; + } else { + print("ext2: Symlinks with destination paths longer than 60 chars unsupported\n"); + return false; + } +} + +static bool ext2_parse_dirent(struct ext2_dir_entry *dir, struct ext2_file_handle *fd, const char *path) { + if (*path != '/') { + panic(true, "ext2: Path does not start in /"); + } + + path++; + + struct ext2_inode current_inode = fd->root_inode; + + bool escape = false; + static char token[256]; + + bool ret; + + const char *cwd = path - 1; // because / + size_t cwd_len = 1; + size_t next_cwd_len = cwd_len; + +next: + memset(token, 0, 256); + + for (size_t i = 0; i < 255 && *path != '/' && *path != '\0'; i++, path++, next_cwd_len++) + token[i] = *path; + + if (*path == '\0') + escape = true; + else + path++, next_cwd_len++; + + uint32_t *alloc_map = create_alloc_map(fd, ¤t_inode); + + for (uint32_t i = 0; i < current_inode.i_size; ) { + // preliminary read + inode_read(dir, i, sizeof(struct ext2_dir_entry), + ¤t_inode, fd, alloc_map); + + // name read + char *name = ext_mem_alloc(dir->name_len + 1); + + memset(name, 0, dir->name_len + 1); + inode_read(name, i + sizeof(struct ext2_dir_entry), dir->name_len, + ¤t_inode, fd, alloc_map); + + int (*strcmpfn)(const char *, const char *) = case_insensitive_fopen ? strcasecmp : strcmp; + + int test = strcmpfn(token, name); + pmm_free(name, dir->name_len + 1); + + if (test == 0) { + if (escape) { + ret = true; + goto out; + } else { + // update the current inode + ext2_get_inode(¤t_inode, fd, dir->inode); + while ((current_inode.i_mode & FMT_MASK) != S_IFDIR) { + if ((current_inode.i_mode & FMT_MASK) == S_IFLNK) { + if (!symlink_to_inode(¤t_inode, fd, cwd, cwd_len)) { + ret = false; + goto out; + } + } else { + print("ext2: Part of path is not directory nor symlink\n"); + ret = false; + goto out; + } + } + pmm_free(alloc_map, current_inode.i_blocks_count * sizeof(uint32_t)); + cwd_len = next_cwd_len; + goto next; + } + } + + i += dir->rec_len; + } + + ret = false; + +out: + pmm_free(alloc_map, current_inode.i_blocks_count * sizeof(uint32_t)); + return ret; +} + +static void ext2_read(struct file_handle *handle, void *buf, uint64_t loc, uint64_t count); +static void ext2_close(struct file_handle *file); + +struct file_handle *ext2_open(struct volume *part, const char *path) { + struct ext2_file_handle *ret = ext_mem_alloc(sizeof(struct ext2_file_handle)); + + ret->part = part; + + volume_read(ret->part, &ret->sb, 1024, sizeof(struct ext2_superblock)); + + struct ext2_superblock *sb = &ret->sb; + + if (sb->s_magic != EXT2_S_MAGIC) { + pmm_free(ret, sizeof(struct ext2_file_handle)); + return NULL; + } + + if (sb->s_rev_level != 0 && + (sb->s_feature_incompat & EXT2_IF_COMPRESSION || + sb->s_feature_incompat & EXT2_IF_INLINE_DATA || + sb->s_feature_incompat & EXT2_FEATURE_INCOMPAT_META_BG)) { + print("ext2: filesystem has unsupported features %x\n", sb->s_feature_incompat); + pmm_free(ret, sizeof(struct ext2_file_handle)); + return NULL; + } + + if (sb->s_rev_level != 0 && sb->s_feature_incompat & EXT2_IF_ENCRYPT) { + print("ext2: WARNING: File system has encryption feature on, stuff may misbehave\n"); + } + + if (sb->s_state == EXT2_FS_UNRECOVERABLE_ERRORS) { + print("ext2: unrecoverable errors found\n"); + pmm_free(ret, sizeof(struct ext2_file_handle)); + return NULL; + } + + ret->block_size = ((uint64_t)1024 << ret->sb.s_log_block_size); + + ext2_get_inode(&ret->root_inode, ret, 2); + + struct ext2_dir_entry entry; + + size_t cwd_len = 0; + char *cwd = ext_mem_alloc(4096); + for (int i = strlen(path) - 1; i > 0; i--) { + if (path[i] == '/' || path[i] == 0) { + cwd_len = i; + break; + } + } + memcpy(cwd, path, cwd_len); + + if (!ext2_parse_dirent(&entry, ret, path)) { + pmm_free(cwd, 4096); + pmm_free(ret, sizeof(struct ext2_file_handle)); + return NULL; + } + + ext2_get_inode(&ret->inode, ret, entry.inode); + + while ((ret->inode.i_mode & FMT_MASK) != S_IFREG) { + if ((ret->inode.i_mode & FMT_MASK) == S_IFLNK) { + if (!symlink_to_inode(&ret->inode, ret, cwd, cwd_len)) { + pmm_free(cwd, 4096); + pmm_free(ret, sizeof(struct ext2_file_handle)); + return NULL; + } + } else { + print("ext2: Entity is not regular file nor symlink\n"); + pmm_free(cwd, 4096); + pmm_free(ret, sizeof(struct ext2_file_handle)); + return NULL; + } + } + + pmm_free(cwd, 4096); + + ret->size = ret->inode.i_size; + + ret->alloc_map = create_alloc_map(ret, &ret->inode); + + struct file_handle *handle = ext_mem_alloc(sizeof(struct file_handle)); + + handle->fd = ret; + handle->read = (void *)ext2_read; + handle->close = (void *)ext2_close; + handle->size = ret->size; + handle->vol = part; +#if defined (UEFI) + handle->efi_part_handle = part->efi_part_handle; +#endif + + return handle; +} + +static void ext2_close(struct file_handle *file) { + struct ext2_file_handle *f = file->fd; + if (f->alloc_map != NULL) { + pmm_free(f->alloc_map, f->inode.i_blocks_count * sizeof(uint32_t)); + } + pmm_free(f, sizeof(struct ext2_file_handle)); +} + +static void ext2_read(struct file_handle *file, void *buf, uint64_t loc, uint64_t count) { + struct ext2_file_handle *f = file->fd; + inode_read(buf, loc, count, &f->inode, f, f->alloc_map); +} + +static struct ext4_extent_header *ext4_find_leaf(struct ext4_extent_header *ext_block, uint32_t read_block, uint64_t block_size, struct volume *part) { + struct ext4_extent_idx *index; + + void *buf = ext_mem_alloc(block_size); + memcpy(buf, ext_block, block_size); + ext_block = buf; + + for (;;) { + index = (struct ext4_extent_idx *)((size_t)ext_block + 12); + + #define EXT4_EXT_MAGIC 0xf30a + if (ext_block->magic != EXT4_EXT_MAGIC) + panic(false, "invalid extent magic"); + + if (ext_block->depth == 0) { + return ext_block; + } + + int i; + for (i = 0; i < ext_block->entries; i++) { + if (read_block < index[i].block) + break; + } + + if (--i < 0) + panic(false, "extent not found"); + + uint64_t block = ((uint64_t)index[i].leaf_hi << 32) | index[i].leaf; + + volume_read(part, buf, (block * block_size), block_size); + ext_block = buf; + } +} + +static int inode_read(void *buf, uint64_t loc, uint64_t count, + struct ext2_inode *inode, struct ext2_file_handle *fd, + uint32_t *alloc_map) { + for (uint64_t progress = 0; progress < count;) { + uint64_t block = (loc + progress) / fd->block_size; + + uint64_t chunk = count - progress; + uint64_t offset = (loc + progress) % fd->block_size; + if (chunk > fd->block_size - offset) + chunk = fd->block_size - offset; + + uint32_t block_index; + + if (inode->i_flags & EXT4_EXTENTS_FLAG) { + struct ext4_extent_header *leaf; + struct ext4_extent *ext; + int i; + + leaf = ext4_find_leaf((struct ext4_extent_header *)inode->i_blocks, block, fd->block_size, fd->part); + + if (!leaf) + panic(false, "invalid extent"); + ext = (struct ext4_extent*)((size_t)leaf + 12); + + for (i = 0; i < leaf->entries; i++) { + if (block < ext[i].block) { + break; + } + } + + if (--i >= 0) { + block -= ext[i].block; + if (block >= ext[i].len) { + panic(false, "block longer than extent"); + } else { + uint64_t start = ((uint64_t)ext[i].start_hi << 32) + ext[i].start; + block_index = start + block; + } + } else { + panic(false, "extent for block not found"); + } + + pmm_free(leaf, fd->block_size); + } else { + block_index = alloc_map[block]; + } + + volume_read(fd->part, buf + progress, (block_index * fd->block_size) + offset, chunk); + + progress += chunk; + } + + return 0; +} + +bool ext2_get_guid(struct guid *guid, struct volume *part) { + struct ext2_superblock sb; + volume_read(part, &sb, 1024, sizeof(struct ext2_superblock)); + + if (sb.s_magic != EXT2_S_MAGIC) + return false; + + ((uint64_t *)guid)[0] = sb.s_uuid[0]; + ((uint64_t *)guid)[1] = sb.s_uuid[1]; + + return true; +} + +char *ext2_get_label(struct volume *part) { + struct ext2_superblock sb; + volume_read(part, &sb, 1024, sizeof(struct ext2_superblock)); + + if (sb.s_magic != EXT2_S_MAGIC) { + return NULL; + } + + if (sb.s_rev_level < 1) { + return NULL; + } + + size_t label_len = strlen((char *)sb.s_volume_name); + if (label_len == 0) { + return NULL; + } + char *ret = ext_mem_alloc(label_len + 1); + strcpy(ret, (char *)sb.s_volume_name); + + return ret; +} diff --git a/common/fs/file.s2.c b/common/fs/file.s2.c index 70bc822b..68fb354f 100644 --- a/common/fs/file.s2.c +++ b/common/fs/file.s2.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -16,12 +17,17 @@ char *fs_get_label(struct volume *part) { if ((ret = fat32_get_label(part)) != NULL) { return ret; } + if ((ret = ext2_get_label(part)) != NULL) { + return ret; + } return NULL; } bool fs_get_guid(struct guid *guid, struct volume *part) { - (void)guid; (void)part; + if (ext2_get_guid(guid, part) == true) { + return true; + } return false; } @@ -50,6 +56,9 @@ struct file_handle *fopen(struct volume *part, const char *filename) { return ret; } + if ((ret = ext2_open(part, filename)) != NULL) { + goto success; + } if ((ret = iso9660_open(part, filename)) != NULL) { goto success; } diff --git a/test.mk b/test.mk index d899ecaf..993e6d4f 100644 --- a/test.mk +++ b/test.mk @@ -32,6 +32,29 @@ mbrtest.hdd: dd if=/dev/zero bs=1M count=0 seek=64 of=mbrtest.hdd echo -e "o\nn\np\n1\n2048\n\nt\n6\na\nw\n" | fdisk mbrtest.hdd -H 16 -S 63 +.PHONY: ext2-test +ext2-test: + $(MAKE) test-clean + $(MAKE) test.hdd + $(MAKE) limine-bios + $(MAKE) limine + $(MAKE) -C test TOOLCHAIN_FILE='$(call SHESCAPE,$(BUILDDIR))/toolchain-files/uefi-x86_64-toolchain.mk' + rm -rf test_image/ + mkdir test_image + sudo losetup -Pf --show test.hdd > loopback_dev + sudo partprobe `cat loopback_dev` + sudo mkfs.ext2 `cat loopback_dev`p1 + sudo mount `cat loopback_dev`p1 test_image + sudo mkdir test_image/boot + sudo cp -rv $(BINDIR)/* test_image/boot/ + sudo cp -rv test/* test_image/boot/ + sync + sudo umount test_image/ + sudo losetup -d `cat loopback_dev` + rm -rf test_image loopback_dev + $(BINDIR)/limine bios-install test.hdd + qemu-system-x86_64 -net none -smp 4 -hda test.hdd -debugcon stdio + .PHONY: fat12-test fat12-test: $(MAKE) test-clean