最近,笔者当然还是在努力写系统,并且在笔者的“滋润”下,我们的cunix
系统已经有一个操作系统的样子了。大家可以看看https://github.com/pengruiyang-cpu/cunix.git,gitee上也有,https://gitee.com/pengruiyang-cpu/cunix.git,有兴趣的当然可以向上面提交代码啦,作为开源与GPL
的狂热热爱者,笔者当然欢迎。
今天我们要聊的这个“诡异的数值”,几乎就是一本活生生的教科书,是笔者在编写cunix
的文件系统
时出现的一个问题。代码笔者同样是放在了github上,https://github.com/pengruiyang-cpu/lessons/tree/master/0x00000001/code,https://gitee.com/pengruiyang-cpu/lessons/tree/master/0x00000001/code
大家可以试试用GCC
编译一下:
gcc mkfs.c bitmap.c -o mkfs
编译出mkfs
,然后试试用它格式化一个磁盘映像。或者干脆格式化一个不用的磁盘也可以。大家会发现,屏幕上显示出的inode
非常少,例如笔者的64GB的U盘(实际57GB可用),竟然只有50000个?
大家最开始得出这个数值一定是怀疑笔者的逻辑写错了,其实并不是,笔者设定的逻辑是,inode
大小是磁盘大小的512分之一,而每个inode
占用64
个字节。所以,57GB
的磁盘,inode
应该有(57GB / 512 / 64
)1867776
个才对,这里竟然只显示50000
个,连三十分之一都不到。
而且,这里的代码笔者自认为写的还算漂亮,下面是笔者开始怀疑的问题代码。
int setup_sb(int fd, __uint32_t cblocks) {
superblock_d.magic = SB_MAGIC;
superblock_d.cblocks = cblocks;
printf("blocks count: %u\n", cblocks);
/* block bitmap start at second block */
/* `>> 3` is faster than ` / 8` */
/* MINODES_USED = 4GB * 64 = 256GB */
/* if you want use 4G inodes, you must have a disk with 128TB (MINODES_USED * 512) */
/* don't forget `ALIGNUP_4096`! else maybe overflow, I did it :-( */
#define BBM_START (0 + 1)
#define BBM_BLOCKS (ALIGNUP_4096(cblocks >> 3))
/* on my computer, a 64GB disk will have 2 million inodes, but it takes about only 50000 */
#define INODES_COUNT (((cblocks * BLOCK_SIZE) >> 9) / sizeof(struct inode))
#define IBM_START (BBM_START + BBM_BLOCKS + 1)
#define IBM_BLOCKS (ALIGNUP_4096(INODES_COUNT >> 3))
#define INODES_START (IBM_START + IBM_START + 1)
#define INODES_BLOCKS (ALIGNUP_4096(INODES_COUNT * sizeof(struct inode)))
#define FIRST_DATA_BLOCK (INODES_START + INODES_BLOCKS + 1)
superblock_d.block_bitmap = BBM_START;
/* inodes size : disk size = 1 : 511 */
/* so, if you use a 64G disk, you have about 2 million inodes */
/* ` >> 9` is faster than ` / 512` */
/* no anyone have a 128TB disk (maybe future), so I didn't check (cinodes > MAX_INODES) */
superblock_d.cinodes = INODES_COUNT;
superblock_d.first_datablock = FIRST_DATA_BLOCK;
printf("inodes count: %u\n", INODES_COUNT);
printf("first data block: %u\n", FIRST_DATA_BLOCK);
/* write to first block */
bpwrite(fd, (char *) &superblock_d, BLOCK_SIZE, 0);
return FIRST_DATA_BLOCK - 1;
#undef BBM_START
#undef BBM_BLOCKS
#undef INODES_START
#undef INODES_BLOCKS
#undef IBM_START
#undef IBM_BLOCKS
#undef INODES_COUNT
#undef FIRST_DATA_BLOCK
}
其中,最可疑的一段代码是:
#define BBM_START (0 + 1)
#define BBM_BLOCKS (ALIGNUP_4096(cblocks >> 3))
/* on my computer, a 64GB disk will have 2 million inodes, but it takes about only 50000 */
#define INODES_COUNT (((cblocks * BLOCK_SIZE) >> 9) / sizeof(struct inode))
#define IBM_START (BBM_START + BBM_BLOCKS + 1)
#define IBM_BLOCKS (ALIGNUP_4096(INODES_COUNT >> 3))
#define INODES_START (IBM_START + IBM_START + 1)
#define INODES_BLOCKS (ALIGNUP_4096(INODES_COUNT * sizeof(struct inode)))
#define FIRST_DATA_BLOCK (INODES_START + INODES_BLOCKS + 1)
superblock_d.block_bitmap = BBM_START;
/* inodes size : disk size = 1 : 511 */
/* so, if you use a 64G disk, you have about 2 million inodes */
/* ` >> 9` is faster than ` / 512` */
/* no anyone have a 128TB disk (maybe future), so I didn't check (cinodes > MAX_INODES) */
superblock_d.cinodes = INODES_COUNT;
superblock_d.first_datablock = FIRST_DATA_BLOCK;
printf("inodes count: %u\n", INODES_COUNT);
printf("first data block: %u\n", FIRST_DATA_BLOCK);
笔者尝试在里面显示了所有变量的信息,就像GDB
调试那样。然而,最终什么也没有收获。所有的变量都正确,笔者再用Python
算了算,结果也是1888207
,和我们用计算器的计算结果几乎完全相等(因为57GB
是一个大约值),这证明这段算法是没有问题的。
那么,问题肯定就出在C语言本身上。笔者吃了一个冰激凌冷静了一下,然后默默输入:
gcc -S mkfs.c
将mkfs.c
编译为汇编语言。这是因为笔者要从汇编层面直接解刨我们编写的代码。
编译出来的mkfs.s
笔者也提交到了github和gitee上,经过笔者的注释与删改,核心部分如下(只对核心部分做了注释)
/* 下面的内容保存在rodata段(只读数据段)中 */
.section .rodata
.LC5:
.string "blocks count: %u\n"
.LC6:
.string "inodes count: %u\n"
.LC7:
.string "first data block: %u\n"
.text
.globl setup_sb
.type setup_sb, @function
setup_sb:
.LFB8:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
/* store arguments */
/* 保存参数到栈 */
/*
-4(%rbp): fd
-8(%rbp): cblocks
*/
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
/* superblock_d.magic = SB_MAGIC */
movl $20090427, 512+superblock_d(%rip)
/* superblock_d.cblocks = cblocks */
movl -8(%rbp), %eax
movl %eax, 516+superblock_d(%rip)
/* printf("blocks count: %u\n", cblocks) */
movl -8(%rbp), %eax
movl %eax, %esi
leaq .LC5(%rip), %rdi
movl $0, %eax
call printf@PLT
/* superblock_d.block_bitmap = BBM_START */
movl $1, 520+superblock_d(%rip)
/* superblock_d.cinodes = INODES_COUNT */
/* %eax = cblocks */
movl -8(%rbp), %eax
/* `sal` means $12 is signed */
/* `sal` 指令意味着操作数12是有符号的 */
sall $12, %eax
shrl $9, %eax
shrl $6, %eax
movl %eax, 528+superblock_d(%rip)
.....
看到第51行笔者顿时就恍然大悟——原来如此!什么意思呢?
前面的一到十一行,是我们在程序中使用的字符串(保存在只读数据段中)以及GCC
为链接器提供的一些信息(包括函数的名称等等)。
而第十四行就开始正式进入我们的处理了。下面两句是编译器在进入函数时的必须处理,作用是备份栈指针rsp
到rbp
(处理器不能直接以rsp为地址来读写),后面会在函数结束的时候还原回来。
/* 保存rbp寄存器的值 */
push rbp
/* 将rsp寄存器备份到rbp以供栈的直接读写 */
mov rbp, rsp
......(函数的操作)
/* 恢复rbp寄存器的值 */
mov rsp, rbp
pop rbp
然后的两行是GCC
编译器在64位模式下的传参数规定,di
是第一个参数,si
是第二个参数,最后在寄存器不够用的时候才会使用栈来传递参数。这样的好处就是速度快(32位不这样做是因为寄存器只有8个,64位有16个),然而坏处就是在汇编语言
调用C语言
时很不方便。GCC
为了后面的处理方便,会在进入函数后将寄存器中的值保存回栈中,这样好像和32位一样。
后面的指令都大同小异,无非就是:
mov 值, 偏移量+superblock_d(%rip)
例如这句汇编代码:
movq $1234567, 123+superblock_d(%rip)
就是将superblock_d变量往后的第123字节,设为一个64位整数 1234567。假设superblock_d的第123个字节就是结构体成员acb,那么这段汇编代码相当于C语言的:
superblock_d.acb = (__uint64_t) 1234567;
那么,第五十一行往后到底是什么意思呢?我们先来看C语言的源代码:
#define INODES_COUNT (((cblocks * BLOCK_SIZE) >> 9) / sizeof(struct inode))
/* inodes size : disk size = 1 : 511 */
/* so, if you use a 64G disk, you have about 2 million inodes */
/* ` >> 9` is faster than ` / 512` */
/* no anyone have a 128TB disk (maybe future), so I didn't check (cinodes > MAX_INODES) */
superblock_d.cinodes = INODES_COUNT;
展开第七行,就是:
superblock_d.cinodes = (((cblocks * BLOCK_SIZE) >> 9) / sizeof(struct inode))
我们知道,结构体inode
的大小是64
字节,所以继续展开,得到:
superblock_d.cinodes = (((cblocks * BLOCK_SIZE) >> 9) / 64)
继续展开,我们要知道BLOCK_SIZE
的定义。他定义在cfs.h
中:
#ifndef INCLUDED_CFS_H
#define INCLUDED_CFS_H
#include <sys/types.h>
#include <assert.h>
#define ALIGNUP_4096(x) (((x) + 0xfff) & 0xfffff000)
#define BLOCK_SIZE 4096
要注意的是,BLOCK_SIZE
并没有声明他的类型,编译器会按照默认的int
类型,也就是有符号的32位整数
。而我们再来看我们的运算:
superblock_d.cinodes = (((cblocks * 4096) >> 9) / 64)
cblocks
变量是一个__uint32_t
的无符号三十二位整数,如果用它来乘一个有符号三十二位整数的话……
没错!就是溢出!况且,这里的变量cblocks
表示块的数量,通常是一个极其大的数值。所以,最后,我们得出结论:
对于INODES_COUNT
的计算算法设计有缺陷,导致其结果溢出。
要解决这一问题也很简单,只要将其中一个类型声 明为64位的无符号整形即可,这样,GCC
会为他匹配一个64位的寄存器,溢出也就不会发生了。
通过这次的崩溃分析,我们学到:在计算精度有限的计算机中,我们不得不谨慎的对待每一次计算,认真设计每一个算法。当然,最重要的是:
你得像笔者一样懂一点汇编!