32位有符号整形的溢出

本文讲述了在C语言编程中遇到的32位有符号整型溢出问题,通过分析代码和汇编语言,揭示了由于类型转换导致的大数值计算错误。作者提供了一个例子,展示如何通过理解溢出来改进算法设计,避免此类问题的发生。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近,笔者当然还是在努力写系统,并且在笔者的“滋润”下,我们的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/codehttps://gitee.com/pengruiyang-cpu/lessons/tree/master/0x00000001/code

大家可以试试用GCC编译一下:
gcc mkfs.c bitmap.c -o mkfs
编译出mkfs,然后试试用它格式化一个磁盘映像。或者干脆格式化一个不用的磁盘也可以。大家会发现,屏幕上显示出的inode非常少,例如笔者的64GB的U盘(实际57GB可用),竟然只有50000个?

能用的inode竟然只有50000多个

大家最开始得出这个数值一定是怀疑笔者的逻辑写错了,其实并不是,笔者设定的逻辑是,inode大小是磁盘大小的512分之一,而每个inode占用64个字节。所以,57GB的磁盘,inode应该有(57GB / 512 / 641867776个才对,这里竟然只显示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是一个大约值),这证明这段算法是没有问题的。

python测试算法

那么,问题肯定就出在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为链接器提供的一些信息(包括函数的名称等等)。

而第十四行就开始正式进入我们的处理了。下面两句是编译器在进入函数时的必须处理,作用是备份栈指针rsprbp(处理器不能直接以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位的寄存器,溢出也就不会发生了。

通过这次的崩溃分析,我们学到:在计算精度有限的计算机中,我们不得不谨慎的对待每一次计算,认真设计每一个算法。当然,最重要的是:

你得像笔者一样懂一点汇编!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值