DevLabs 发表于 2014-10-26 22:39:10

从坑里刚爬出来, 大家来围观一下, 涨涨姿势.

从坑里刚爬出来, 发个帖子.

先说起因.
因为要用到unix时间戳, 可恶的是AVR-GCC自带的库里面居然不包括time.h, 还好我要尔不高, 我只用到mktime()和localtime()两个函数而已, 而自己造轮子的话还是略有难度, 于是下载了glibc的源代码, 打算把这两个函数扒出来, 结果一看代码傻眼了, 比我想像的复杂, 那好吧, 先Google再说.所幸搜索之一通后找到一个从linux内核里扒出来的mktime()版本和一个网友自己写的localtime().
下面这个就是从linux内核里扒出来的mktime():

static time_t _mktime(int year,
                      int mon,
                      int day,
                      int hour,
                      int min,
                      int sec)
{
    if (0 >= (int)(mon -= 2))
    {                     /* 1..12 -> 11,12,1..10 */
      mon += 12;          /* Puts Feb last since it has leap day */
      year -= 1;
    }

    return ((((time_t)(year/4 - year/100 + year/400 + 367*mon/12 + day) + year*365L - 719499L
             )*24 + hour    /* now have hours */
            )*60 + min      /* now have minutes */
      )*60 + sec;         /* finally seconds */
}

然后在电脑使用GCC编译, 测试, 没问题之后用AVR-GCC编译, 下载到板子上一试, 居然有问题.

先说一下时间相关的代码逻辑:
先从RTC(PCF8563)中获取日期信息, 填入struct tm结构体, 然后用mktime()转换成unix时间戳. RTC获取时间的接口函数返回的即是这个时间戳.
在外部使用细分时间的时候再将获取到的时间戳使用localtime()转换回来, 得到日期等信息.

现在可以描述一下问题现像了:
当初始化的时间(绝对不会错)读回来后用localtime()转出来一看, 驴头不对马嘴.
更惨的是手头没调试器, 只能从串口打印调试信息.

于是打印读到的unix时间戳一看, 错的. 很自然的怀疑是调用mktime()的时候结构体信息填错了, 打印了一堆信息后证明没问题.

然后怀疑缓冲溢出, 野指针等, 结果证明均无问题. 在源文件里差写了1/5的调试代码后, 终于怀疑到mktime()头上来了.

无论如何, 我还是有点不相信, 一来这是在linux内核里扒出来的, 二来我已经在电脑上用GCC编译测试过了, 确实没问题.再说这段代码这么短, 按过程手算一遍结果都是对的啊.

最先怀疑的是那个return表达式太长, 编译器在处理表达式时出了问题. 虽然连我自己都不相信, 但我依然还是将它拆分成几部分. 结果证明, 与表达式无关, 但真相就快浮出水面了.

先看一段代码.

int main(void)
{
    uint32_t i;
    int j;

    j = 1999;

    i = j * 365 - 719499;

    return 0;
}

你能看出来这段代码那里有问题吗? 很明显没什么问题, 在电脑上计算结果也是对的, 无任何警告, 当然在AVR-GCC里也没任何警告, 如图, 使用-Wall选项警告全开哦.



诡异的是算出来的结果是错的. 或许我说过这句话后你已经知道是怎么回事了, 但是mktime()这个函数里看起来可不是那么明显.

然后打印了好几次调试信息后, 我把代码改成这样:

int main(void)
{
    uint32_t i;
    int j;

    j = 1999;

    i = 1999 * 365 - 719499;

    return 0;
}


这次TM终于报了个警告: warning: integer overflow in expression !!

草, 瞬间明白了, 栽到整形提升上了.

如果还有没明白的童鞋, 我来解释一下:

参数j是int型的, 365 和 719499 都足够用int表示, 所以右值的类型是int型. 很不幸的是当乘法计算完毕后结果就溢出了! 为什么会溢出呢? 因为在AVR-GCC中, int类型占用的空间为两个字节! 即sizeof(int) = 2. 在PC上不会溢出的原因是因为在PC平台上int类型占用4个字节!

知道原因后解决就很简单了, 因为是在计算乘法时溢出的, 所以将365改为365L即可, 或者将j强制转换为uint32类型亦可.

(全文完)

后记: 折腾了大半天, 累死我了. 昨天睡觉扭了脖子, 疼的很, 先睡了.

salahading 发表于 2014-10-26 23:07:21

恩恩                           

Free_Bird 发表于 2014-10-26 23:21:06

恩,涨了点知识,以后会注意

wshtyr 发表于 2014-10-27 00:24:58

我一般这样处理:

如果是简单的计算或循环计数,用int
如果涉及到较大的数值就用uint32_t 或 uint16_t,具体用哪个需要估算中间值的范围

记得刚学C语言的时候书上说int是16位的,实际试验的时候发现是32位的,所以对这个映象很深刻

radar_12345 发表于 2014-10-27 07:38:48

还是那句话: 现象越奇怪,错误越低级

jiang887786 发表于 2014-10-27 07:58:49

好!楼主辛苦了!

fghfguytu 发表于 2014-10-27 07:59:31

涨知识了,记住了

amigenius 发表于 2014-10-27 08:07:23

这是基本常识,折腾只能怪自己基础不扎实

bzbs 发表于 2014-10-27 08:14:37

radar_12345 发表于 2014-10-27 07:38
还是那句话: 现象越奇怪,错误越低级

这个才是经验总结啊

DevLabs 发表于 2014-10-27 08:23:06

wshtyr 发表于 2014-10-27 00:24
我一般这样处理:

如果是简单的计算或循环计数,用int


不同平台之间程序的移植尤其要注意,很多PC平台上的代码用的是标准类型,而糟糕的是不同平台对标准类型的长度定义不一样,溢出了都不清楚在哪里,更别说警告了。

风轻云淡 发表于 2014-10-27 08:24:54

楼主用的什么编辑器,配色看起来挺舒服的

tonyhobby 发表于 2014-10-27 08:27:07

是蛮舒服的

McuPlayer 发表于 2014-10-27 08:27:28

做嵌入式多了,逐步就抛弃了使用int
我的程序,只使用U8 U16 U32 U64和S8 S16 S32 S64这种二次定义的类型
一是移植的时候方便,二是数据sizeof是确定的

当然,从执行速度和产生代码大小上看,int因为和CPU的字长一致,往往是最好的

DevLabs 发表于 2014-10-27 08:27:43

amigenius 发表于 2014-10-27 08:07
这是基本常识,折腾只能怪自己基础不扎实

你肯定没仔细看贴。这个问题估计大部分人不会一眼看出来。

zsg211550 发表于 2014-10-27 08:33:26

涨知识,Mark!

amigenius 发表于 2014-10-27 08:40:16

DevLabs 发表于 2014-10-27 08:27
你肯定没仔细看贴。这个问题估计大部分人不会一眼看出来。

我肯定没仔细看,因为这种问题,瞧一眼就知道是什么问题了。对于第二句话,您要么是把大部分人看扁了,要么是您指只有中国人才算是人。

yu_studio 发表于 2014-10-27 08:51:46

int类型在不同的平台上长度是不一样的
所以我一般不用int类型。
16位我用short,32位用long

蓝蓝的恋 发表于 2014-10-27 08:52:05

一起来长长姿势~

DevLabs 发表于 2014-10-27 08:57:49

风轻云淡 发表于 2014-10-27 08:24
楼主用的什么编辑器,配色看起来挺舒服的

gVim,配色是solarized。

dianyuan 发表于 2014-10-27 09:09:40

radar_12345 发表于 2014-10-27 07:38
还是那句话: 现象越奇怪,错误越低级

非常有道理

Vmao 发表于 2014-10-27 09:15:00

仿真器你没用上?

DevLabs 发表于 2014-10-27 10:32:58

Vmao 发表于 2014-10-27 09:15
仿真器你没用上?

手边没有仿真器.
后来装了AVR studio软件仿真, 但AVR Studio软件仿真跑的巨慢, 放弃.

hongbo3636 发表于 2014-10-27 10:42:36

radar_12345 发表于 2014-10-27 07:38
还是那句话: 现象越奇怪,错误越低级

总结的很正确。

771494781 发表于 2014-10-27 13:08:39

涨知识            

wshtyr 发表于 2014-10-27 20:43:10

DevLabs 发表于 2014-10-27 10:32
手边没有仿真器.
后来装了AVR studio软件仿真, 但AVR Studio软件仿真跑的巨慢, 放弃. ...

gVim握爪{:lol:}

刚学的时候想砸键盘,现在越来越好用了

CMika 发表于 2014-10-27 21:07:33

现象越奇怪,错误越低级      {:handshake:}

OOXX110 发表于 2014-10-28 01:27:24

现象越奇怪,错误越低级。超赞同!

晨星 发表于 2014-10-28 06:55:17

学习一下

DevLabs 发表于 2014-10-28 07:25:43

wshtyr 发表于 2014-10-27 20:43
gVim握爪

刚学的时候想砸键盘,现在越来越好用了

现在使用别的编辑器也不自觉的去按ESC{:lol:}

wei669 发表于 2014-10-28 09:54:52

有意思,以前也碰到过

takashiki 发表于 2014-10-28 10:04:32

yu_studio 发表于 2014-10-27 08:51
int类型在不同的平台上长度是不一样的
所以我一般不用int类型。
16位我用short,32位用long ...

万一某个编译器上long是64位你怎么办,还是免不了掉坑里
C语言跨平台上,其实他们自己也做过很多努力的,只是使用者以各种原因忽略。C语言可移植数据类型:#include <stdint.h>

雨中的风铃 发表于 2014-10-29 12:20:57

涨知识了,记住了

DevLabs 发表于 2014-10-29 12:33:42

takashiki 发表于 2014-10-28 10:04
万一某个编译器上long是64位你怎么办,还是免不了掉坑里
C语言跨平台上,其实他们自己也做过很多努力的, ...

现在我一般使用 <stdint.h> 和 <stdbool.h>.
然后字符串数组为了使用库函数时编译器不产生警告, 还是使用 char.

alwxkxk 发表于 2014-10-29 12:35:39

radar_12345 发表于 2014-10-27 07:38
还是那句话: 现象越奇怪,错误越低级

支持这观点,真神!

落叶随风 发表于 2014-10-29 12:40:03

习惯用uint32、uint16了,一般for里的i可能会用下int

llj1007 发表于 2014-10-29 14:18:32

有时候一些越低级的错误越难发现

zhumingxing6 发表于 2014-10-29 14:29:17

涨知识了,学习学习

markdif 发表于 2014-10-29 14:40:18

不错,涨姿势了。。。

286116049 发表于 2014-10-29 15:03:54

wshtyr 发表于 2014-10-27 00:24
我一般这样处理:

如果是简单的计算或循环计数,用int


int不固定是16位或32位,选16位芯片开发,在开发环境中认为16位,32位芯片,开发环境就认为是32位。好像short型是固定16位的。

lj11hy 发表于 2014-10-29 15:29:57

{:smile:}{:smile:}{:smile:}

比特 发表于 2014-10-29 15:39:14

C99都有stdint.h,用这个类型定义变量吧

ordman 发表于 2014-10-29 15:45:56

为了避免这类问题,我一般使用u8,u16,u32

voidwalker2012 发表于 2014-10-29 16:24:52

现象越奇怪,错误越低级

451006071 发表于 2014-11-4 19:59:27

对于这类问题我只能说呵呵,长见识了{:smile:}
页: [1]
查看完整版本: 从坑里刚爬出来, 大家来围观一下, 涨涨姿势.