0%

由整数类型溢出导致的英雄联盟峡谷惨案

最近,英雄联盟迎来了 S11 赛季的版本大更新。作为一名从 S2 一路走过来的老玩家,我自然也是非常的关注。除了发生了巨变的装备系统之外,各种各样的 bug,也是在玩家之间炽手可热的话题。这其中,较为严重的一个 bug 之一,就是打野英雄在特定的情况下,可以对野怪一击造成 -2147483648 的伤害。熟悉英雄联盟的玩家应该知道,21 亿在游戏中可以说是一个天文数字。哪怕是血量最多的野怪,在这个伤害面前也会瞬间灰飞烟灭,这简直是有史以来最大的峡谷惨案。

那为什么会出现 -2147483648 这个大的数字呢?或者说,为什么就恰好就是这样一个数字呢?这个数看起来好像并没有什么规律,难道是由于程序异常导致某些数字累加或者累乘造成的?还是游戏开发者留下的一个类似于彩蛋的东西呢?这就是我今天要跟大家分享的内容——从编程的角度来分析这个 21 亿的神秘数字到底是怎么产生的。而这其中所涉及到的计算机知识,都可以作为面试题了!

要想搞清楚这个问题,首先要了解三个概念:二进制、整数类型和补码。

首先说二进制。无论你是否学过计算机,二进制这个词,多多少少你都可能听到过。那什么是二进制呢?首先我们来看下日常生活中我们广泛使用的十进制。十进制由十个数字符号组成,从 0 到 9。在计数过程中,逢十进一。比如说,9 的下一个数就需要进一位变为 10,19 的下一个数需要进一位变为 20,99 的下一个数需要进一位变成 100。同理,二进制就两个数字符号, 0 和 1,计数时逢二进一。比如说,0 就是 0,1 就是 1,十进制的 2 在二进制中就是 10,十进制的 3 在二进制中就是 11,4 则为 100,5 则为 101,以此类推。

那为什么要提到二进制呢?这是因为,当今我们日常生活中所使用的计算机几乎都是大规模集成电路机,而这种计算机都是基于二进制来实现的。无论你平常见到的数字还是文字,甚至图像影音等等一切数据,最后在计算中都要转化成二进制的形式来存储或者计算。所以二进制可以说是计算机的灵魂,只有知道什么是二进制才能理解后面的内容。

接下来再说整数类型。虽然计算机中的底层数据都要转化成二进制的形式,但是如果直接让程序员使用二进制进行编程则是一件非常麻烦事,毕竟二进制这个东西不是很直观,不利于人类直接操作。所以,为了方便广大编程人员更容易地编程,各种语言的设计者都对二进制做了进一步的封装。而整数类型就是在二进制的基础上,一个用于在编程中存放整数的一种数据类型,一般我们也称之为 int type。比如说,我现在想在程序中存放一个数字 100,那我可以定义一个整数类型的变量 a,用 a 来存放 100 这个数。而游戏中,伤害值都是以整数的形式呈现的,所以游戏开发者大概率使用整数类型来存储和计算伤害值。

不过这里存在一个问题,就是现实世界中,整数的个数是无限的。我们可以从 0 到正无穷,也可以从 0 到负无穷。但是在计算机中,我们的存储空间是有限的,这是一种在物理层面上的限制。试想一下,如果我们想在计算机中存放一个无限大的数,那这个无限大的数转化成二进制后的位数也是无穷多位数,计算机不可能存放无限多位的数据。所以,我们要对存放的数据大小加以限制。对于一个整数类型,一般会分配给它 32 个比特的空间。看到这,有的同学可能会问啥是比特啊?还记得刚才说到的二进制么,二进制中的一位数就是一个比特(bit)。比如,二进制的 4 为 100,占 3 个比特。

回到整数类型。刚才我们说道,一个整数类型被分配了 32 个比特的空间。其中,我们把第一个比特位拿出来作为符号位,0 代表正数,1 代表负数。剩下的 31 个比特位,我们用来存放数字。说得再通俗易懂一些,就是你可想象有 32 个格子排成一行,每个格子当中只能放一个数,0 或者 1。第一个格子用 0 和 1 分别表示正数和负数,剩下 31 个格子则用二进制的形式来存放想要表达的数字。

比如,我想用一个整数类型存放 50 这个数。我们先把十进制的 50 转化成二进制的形式即 110010,在 32 比特中,第一位我们用 0 表示这是个正数,用后六位存放 110010,而中间的二十五位我们用 0 来填充。

那如果想表达 -50 呢?这就需要我们了解第三个概念补码了。先说下补码的计算规则,刚刚我们算的 50 的 32 位二进制形式是 00…00110010,首先将所有位数上的数值取反,也就是 1 变为 0,0 变为 1,得到 11…11001101,这个我们称之为反码。然后在反码的基础上,我们在最后一位上作 +1 操作,则变成 11…11001110。那么这串数字,我们便称之为补码。在整数类型中,负数的存放形式跟正数的区别就是,负数存的是补码形式,而正数存的是原码形式。

那为什么负数要用补码的形式存储呢?简单来说,就是大规模集成电路机中,加法运算是最为简单的。为了使计算机能够轻松计算减法,所以就把负数转化成为补码的形式,即将 A - B 转化成 A + (-B) 的形式,来方便计算机进行加减计算。具体有关原码、反码、补码的知识,这里就不多做展开了,以后有机会再跟大家详细分享。

其实就算你没有理解补码的概念也不要紧,你只需要知道我们平常所用到的十进制的整数,在编程中是用 32 比特的整数类型存放就可以了。既然每个整数类型所分配的位数是固定的,都是 32 比特,那我们就可以计算一个整数类型所能表达的数字范围了。因为只有 31 位是真正用来表示数字的,每个位上有两种可能,所以范围即为 -231 ~ 231 - 1。而超过这个范围的数,就不能够用整数类型来存放了。

讲到这里,我们终于可以回到一开始的问题了,就是 -2147483648 这个神秘数字到底是怎么来的?当我们把 -231 算出来之后就会发现,-231 就是 -2147483648!惊不惊喜,意不意外!也就是说,这个 21 亿的伤害恰好是一个整数类型所能表达的最大负边界值了。是不是有一种谜底终于揭晓了的感觉!所以说,之所以打野英雄能造成这么高的伤害,极有可能是由于触发了某种 bug 导致伤害计算异常,使得伤害值溢出了整形类型所能表达的范围,而整数类型的下界最大便是 -231,所以伤害最终就变成了 -2147483648 这个数字。

没想到这个最近火遍全网的打野一刀秒杀野怪的 bug,居然是一个关于整数类型数值溢出的知识。所以说,掌握计算机思维,确确实实可以解决一些生活中遇到的问题。

最后说几句题外话。一款游戏虽然在内测以及公测阶段能够发现并解决大部分显而易见的问题,但并不能保证在正式版本就一定不存在 bug。所以我们要对开发团队给予一定的信任和耐心,在等待 bug 修复前不要故意利用恶性 bug 来破坏游戏平衡,影响其它玩家的游戏体验,维护良好的游戏环境人人有责哦。


我是因特马,一个爱分享的斜杠程序员~

欢迎关注我的公众号:因特马

  • 本文作者: 因特马
  • 本文链接: https://www.interhorse.cn/a/1583871330/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!