美文网首页工作生活
Bug隐藏在简单程序背后

Bug隐藏在简单程序背后

作者: 一猿小讲 | 来源:发表于2019-07-02 22:15 被阅读0次

    越简单,越玄机,越不简单

    曾记得几年前做培训老师的时候,在辅导学员 Java 面试题过程中,总是提醒学员“越是简单的面试题,其中的玄机就越多,需要学员有相当深厚的功力去面对”。多年从事程序开发以来,现在回头想想,还真是那么回事儿。今天转变一下分享角度,就不聊技术架构啦,咱们一起聊聊那些简单的程序代码背后。

    01. 刚入猿门(懵懂的小白)

    多一点不行,少一点也不行。

    从事金融的程序猿都知道,代码实现功能经常跟钱打交道,钱多一点不行,钱少一点也不行。如果你实现的付款功能,向客户少付了一分钱,客户是否能忍?另外信用卡还款,少了 1 分钱,算你违约,你是否能忍?向你女朋友转账 520 元红包,却到账 250 元,你女朋友是否能忍?闲话不多说,直接上代码。

    double a = 1;

    double b = 0.99;

    System.out.println(a - b);

    肯定你们中也有一部分,坚信这段代码运行结果很简单不是 0.01 么?!很久之前如果问我结果是什么,我也会毫不犹豫的答道 0.01,然而真实的结果却是:0.010000000000000009。如果该段逻辑实现的是提现功能,那会不会损失大发了。

    说说原因:你们都知道,计算机进行的是二进制运算,然而问题在于转换为二进制的时候,有些数字不能完全转换,只能无限接近于原本的值,由于二进制无法准确表示 0.99 ,就像十进制无法准确表示 1/3 一样,所以必定会有精度损失。

    讲讲正解:一般遇到这种,需要用到浮点数运算的地方,都可以使用 java.math.BigDecimal。

    BigDecimal a = new BigDecimal(1);

    BigDecimal b = new BigDecimal(0.99);

    System.out.println(a.subtract(b));

    程序跑起来看看效果,一看到结果会惊呆你们。

    真实的结果输出变成了:0.0100000000000000088817841970012523233890533447265625。

    又损失了一点。枉费你们激情满满,从一个坑又带到另一个坑,不靠谱啊。你们,莫着急,我们不妨把参数改成字符串试试。敲黑板,拨云见日水落石出的时刻到了。

    BigDecimal a = new BigDecimal("1");

    BigDecimal b = new BigDecimal("0.99");

    System.out.println(a.subtract(b));

    程序跑起来一窥究竟,期待良久的结果 0.01 终于正常算出来了。

    我有话说:如果在程序中直接使用 double 进行计算,会造成精度损失,有可能会引起一些莫名奇妙的 bug;如果用 double 来构造 BigDecimal 依然会有精度损失;请你们铭记:直接使用字符串来构造 BigDecimal,是绝对没有精度损失的。

    02. 久居猿门(经验丰富的码农)

    吐血的 Bug,阴沟里翻船。

    我曾经带着兄弟做过一个日志归集的项目,用 elasticsearch 存储采集的日志。由于采集的日志会逐日增多,考虑到系统长期平稳运行,需要每天跑定时任务清理 60 天前的日志信息,用于释放磁盘内存空间。

    日志归集项目上线没过多久,突然发现,2 周前的日志数据貌似丢失了,生产无小事,小事更不能忽视,于是就跟兄弟们一起排查、分析代码,但是没发现逻辑上的问题漏洞。但第二天同样的问题,又规律性的再次发生,于是,兄弟们的焦点便集中到了“定时清理的任务”上。左查右查依然没发现问题,只能一步一步的进行 Debug 跟踪调试。

    很难令人想象,问题就出现在LOG_DATA_INVALID_DATE 这个 long 型常量上。

    说说原因:

    public static final long LOG_DATA_INVALID_DATE = 60 * 24 * 3600 * 1000; 

    按道理表示 60 天的时间 60 * 24 * 3600 * 1000 的值应该是 5184000000 的,但是它实际值却是 889032704,大约 10 天时间。坑爹啊,最后发现居然是 int 在计算过程中的溢出,太隐晦的 bug 了。排查问题过程很痛苦,解决问题的方式却很简单,任意一个常量上加 L,转成 long 型就好了。

    讲讲正解:

    public static final long LOG_DATA_INVALID_DATE = 60L * 24 * 3600 * 1000; 

    我有话说:现在想想,这种 Bug 确实是挺难查的。不过稍微细心一点或者借助 FindBugs 等一些工具来扫描一下,这样的 Bug 应该都可以避免。编码不易,且码且 Debug。

    03. 猿门起飞(装牛 X 的程序员)

    一行代码,蒸发 6,447,277,680 元!

    去年由于币圈的疯狂炒作,导致区块链概念深入到每个人的骨髓,就连跳广场舞的大妈、卖书的大爷都参与跟风,喜欢追新的我当然也不会放过。我用两天时间自学了 Solidity 语言,帮别人写了个发行代币的智能合约代码,人家借势赚了个盆满钵满,出于感激,我还赚了个大红包。

    跑偏了,咱们今天不聊赚红包这事儿,还是分享区块链业界一个智能合约的普遍漏洞吧。

    案例一:随着BEC智能合约的漏洞的爆出,被黑客利用,瞬间套现抛售大额BEC,60亿在瞬间归零。

    案例二:距BEC现重大漏洞几近归零仅时隔三天,SMT等多个智能合约再曝漏洞,交易平台迅速停止重提币业务。

    说说原因:摘取 BEC 部分智能合约代码进行分析。

    稍微写过程序的都能对上图代码理解个八九不离十,就是批量给人转账,函数入参 _receivers 是转给哪些人,_value 是每个人转多少,然后计算一下:你要发送的总金额 = 发送的人数 * 发送的金额,最后从你账户余额中减去你要发送的总金额。

    那么问题出现在哪儿呢?

    分析一: 你要发送的总金额 = 发送的人数 * 发送的金额

    uint256 amount = uint256(cnt) * _value;

    当攻击者传入很大的 value 数值,使 uint256(cnt) * value 后超过 unit256 的最大值使其溢出,便可导致 amount 的值变为 0。

    分析二:你的账户剩余余额 = 你账户金额 - 你要发送的总金额。

    balances[msg.sender] = balances[msg.sender].sub(amount);

    那么当 amount 为0时,你的账户显然不会有任何变化。

    我有话说:就一个简单的溢出漏洞,蒸发 6,447,277,680 元,导致 BEC 代币的市值接近归 0。而这一切,竟然是因为一个简单至极的程序Bug!

    04. 写在最后

    最后想分享的是,作为程序猿,诸多看似很简单的程序代码逻辑,跑起来却差强人意,匪夷所思。Coding 是个精细活,所以你们研发过程中一定要仔细,稍微细心一点会屏蔽很多 Bug,会避免很多损失。希望你们能够透过现象看本质、知其然并且知其所以然。

    欢迎关注微信公众号“一猿小讲”了解更多精彩分享。

    相关文章

      网友评论

        本文标题:Bug隐藏在简单程序背后

        本文链接:https://www.haomeiwen.com/subject/lyjxhctx.html