惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
Forbes - Security
Forbes - Security
The Last Watchdog
The Last Watchdog
TaoSecurity Blog
TaoSecurity Blog
Schneier on Security
Schneier on Security
SecWiki News
SecWiki News
V
Vulnerabilities – Threatpost
Project Zero
Project Zero
O
OpenAI News
W
WeLiveSecurity
Security Archives - TechRepublic
Security Archives - TechRepublic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
H
Hacker News: Front Page
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
Help Net Security
Help Net Security
P
Privacy & Cybersecurity Law Blog
K
Kaspersky official blog
S
Security @ Cisco Blogs
Latest news
Latest news
AWS News Blog
AWS News Blog
U
Unit 42
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
S
Secure Thoughts
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Know Your Adversary
Know Your Adversary
Scott Helme
Scott Helme
博客园 - 司徒正美
B
Blog RSS Feed
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
Docker
Google Online Security Blog
Google Online Security Blog
Jina AI
Jina AI
aimingoo的专栏
aimingoo的专栏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Last Week in AI
Last Week in AI
月光博客
月光博客
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
小众软件
小众软件

博客园 - SoRoMan

Scene Management - Scene Graph 思考:矩阵的级联顺序与坐标系的关系 MFC的一些宏的整理 (DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE) 关于STL Improve Performance of C++ Codes (2) -- 如何消除临时对象? Improve Performance of C++ Codes (1) -- 使用初始化列表还是赋值语句? 关于万向节死锁(Gimbal Lock)(续) 思考:矩阵及变换,以及矩阵在DirectX和OpenGL中的运用问题:左乘/右乘,行优先/列优先,... 读书笔记:Writing Solid Code (3) 读书笔记:Writing Solid Code (2) 读书笔记:Writing Solid Code A 3D Real Time Fluid Solver Demo (三维实时流体解算器) 笔记:Delaunay三角剖分(Delaunay Triangulation)相关知识 学习经验:逐步建立学习资源列表-计算机图形学 SL专题6:谈谈Second Life中的游戏制作(Game Dev in SL) SL专题4:Second Life畅销商品 SL专题3:Second Life市场分析 SL专题2:加入并熟悉Second Life世界 SL专题1:Second Life(第二生命)--迟早火爆世界的游戏与Web的集大成者
读书笔记:Writing Solid Code (4)
SoRoMan · 2008-01-19 · via 博客园 - SoRoMan

读书笔记:Writing Solid Code (1):
http://www.cnblogs.com/soroman/archive/2007/08/06/845465.html
读书笔记:Writing Solid Code (2):
http://www.cnblogs.com/soroman/archive/2007/12/22/1010870.html
读书笔记:Writing Solid Code (3):
http://www.cnblogs.com/soroman/archive/2007/12/25/1014142.htmlGo on writting solid code...

-----------------------------------
Chapter 6
Risky Business
危险的事情
-----------------------------------

Summary:
Given the numerous implementation possibilities for a given function, it
should come as no surprise that some implementations will be more errorprone
than others. The key to writing robust functions is to exchange risky
algorithms and language idioms for alternatives that have proven to be
comparably efficient yet much safer. At one extreme this can mean using
unambiguous data types; at the other it can mean tossing out an entire design
simply because it would be difficult, or impossible, to test.

概述:
如果一个函数有很多实现方案的可能,那么一定是其中一些实现比另一些出错的可能性更大。实现稳定的函数的关键是不用带风险的算法和语言习惯,而是去用那些同样高效但是很安全的方案。在一个极端下,这意味着使用无歧义的数据类型,在另一个极端下,这意味着必须扔掉全部的设计,仅仅是因为它是很难或者不可能被测试的。

Guidlines:
6.1.Use well-defined data types.
使用定义明确的数据类型。

【注】当ANSI委员会看到C运行在各种平台上,它们看到C不是一个象用户认为的那样的一个portable的语言。并仅仅是因为C标准库在不同的系
统上不一样,还因为预处理器和语言本身。ANSI标准委员会对大部分方面作了标准化,但是却忽略了基本数据类型。ANSI没有具体的定义int,char,long,而是留下具体实现给编译器厂商。结果,有些ANSI标准的编译器支持32位整型和带符号的字符型,另外一些编译器可能支持16位整型和无符号char型。
那么看看下面的代码:

1char ch;
2ch = 0xFF;
3if (ch == 0xFF)
4{
5 
6}

上面的判断到底是true or false,你永远也不知道,因为这依赖于编译器的实现。如果是无符号char型,那么结果是true,如果是带符号char型,那么结果就是false。还有象类似:int reg = 3;虽然reg被定义成int,reg可以是signed或者unsigned,还是取决于你的编译器。你必须使用signed int或者unsigned int。
short是多大?int呢?long呢?ANSI标准并没有说,而是留给编译器的作者去决定。标准没有规定的原因是考虑到兼容以前的代码。

6.2.Always ask, "Can this variable or expression over- or underflow?"
时刻问自己:”这个变量或者表达式溢出了吗?”

【注】看看下面的代码:

 1# include <limits.h> /*Pull in UCHARMAX. */
 2char chToLower[UCHARMAX+1];
 3void Bui1dToLowerTable(void/* ASCII version */
 4{
 5 unsigned char ch;
 6 /* First set every character to itself. */
 7 for(ch = 0; ch <= UCHARMAX; ch++)
 8  chToLower[ch] = ch:
 9 /* Now poke lowercase letters into the uppercase slots . */
10 for (ch = 'A' ; ch <= 'Z'; ch++ )
11  chToLower[ch] = ch + 'a' - 'A' :
12}

这段代码的问题在于ch=UCHARMAX的时候,再加1,会导致等于0,所以造成死循环。上面是Overflow的问题,同样存在Underflow的问题:

 1void *memchr(void *pv, unsigned char ch, size_t size)
 2{
 3 unsigned char *pch = (unsigned char *)pv;
 4 while (--size >= 0)
 5 {
 6  if(*pch == ch)
 7   return (pch);
 8  pch++;
 9 }

10 return(NULL);
11}


如果size是unsigned的话,当size=0后再进行递减操作的话,那么就会翻转成unsigned型的size_t的最大值。好消息是如果你按照我在第四章所建议的那样去检查的话,你就可以发现这些溢出bugs。

6.3.Implement "the task" just once.
只实现“任务”一次。

【注】设想为了表示一个文档处理程序中的层次状的Window结构,设计如下结构体:

1typedef struct WINDOW
2{
3 struct WINDOW *pwndChild; /* NULL if no children */
4 struct WINDOW *pwndSibling; /* NULL if no brothers or sisters */
5 char *strWndTitle;
6}
 window; /* Naming: wnd, *pwnd */

现在表示所有的Window可以由该结构体节点构成的二叉树来表示,注意其中的Root节点实际上只有成员pwndChild是有意义的--指向所有top-level的windows, 因为没有兄弟姐妹和Title,而且Root是不能move,hide,or delete的,所以有人将Root节点简化成其中一个成员,即用指针pwndRootChildren来表示,这样至少可以减少空间。于是AddChild(添加一个子window到已存在的window节点下)的实现如下:

 1/* pwndRootChildren is the pointer to the list of top-level
 2* windows,such as the menu bar and the main document windows.
 3*/

 4static window *pwndRootChildren = NULL;
 5
 6void AddChild(window *pwndParent, window *pwndNewBorn)
 7{
 8 /* New windows may have c h i l d r e n but not s i b l i n g s  */
 9 ASSERT(pwndNewB0rn->pwndSibling == NULL);
10 if(pwndparent == NULL)
11 {
12  /* Add window t o the t o p - l e v e l root l i s t . */
13  pwndNewBorn->pwndSibling = pwndRootChildren;
14  pwndRootChildren = pwndNewBorn;
15 }

16 else
17 {
18  /* If Parent's first child. start a new sibling chain;
19   * otherwise, add child to the end of the existing
20   * sibling chain.
21   */

22  if (pwndparent->pwndChild == NULL)
23   pwndparent->pwndChild = pwndNewBorn;
24  else
25  {
26   window *pwnd = pwndparent->pwndChild;
27   while (pwnd->pwndSibling != NULL)
28   pwnd = pwnd->pwndSibling;
29   pwnd->pwndSibling = pwndNewBorn;
30  }

31 }

32}

上述代码至少违背了编写bug-free代码的三个原则:
1.Don't accept special purpose arguments such as the NULL pointer.
2.Implement your design, not something that approximates it.
3.The third principle is new: Strive to make every function perform its task exactly one time

前俩个原则前面已经说过,第三个是新的。上面的代码含有三个不同的插入路径,直觉告诉我们越多的路径越可能导致bugs。这相当于一个任务需要三次“实现”才能完成。尽量用一次“实现”搞定一个任务。上面的代码的改进可以是将Root当作普通的节点,这样逻辑就比较简单了,不用处理Root节点这一特殊情况。代码如下:

 1/* pwndDisplay points to the root-level window. which is
 2* a1located during program initialization .*/

 3window *pwndDisplay = NULL;
 4
 5void AddChild(window *pwndParent, window *pwndNewBorn)
 6{
 7/* New windows may have children but not siblings  */
 8ASSERT(pwndNewBorn->pwndSibling == NULL);
 9/* If Parent's first child, start a new sibling chain;
10* otherwise, add child to the end of the existing sibling chain.*/

11
12if(pwndparent->pwndChild == NULL)
13  pwndparent->pwndChild = pwndNewBorn;
14else
15{
16 window *pwnd = pwndparent->pwndChild;
17 while (pwnd->pwndSibiing != NULL)
18  pwnd = pwnd->pwndSibling;
19  pwnd->pwndSibling = pwndNewBorn;
20 }

21}

6.4.Get rid of extraneous if statements.
处理无关的if语句。

【注】上面改进的AddChild虽然比最开始的要好,但是它还是要做“两次”,还是包含两个路径。实际上,上面的算法都是以Window为中心的,如果换成以Pointer为中心,那么就有:

 1void AddChild(window *pwndParent, window *pwndNewBorn)
 2{
 3 window **ppwndNext;
 4 /* New windows may have c h i l d r e n but not s i b l i n g s  */
 5 ASSERT(pwndNewBorn->pwndSibling == NULL):
 6 /* Traverse the sibling chain using a pointer - centric
 7 * algorithm . We set ppwndNext to point at
 8 * pwndparent->pwndChild since the latter pointer
 9 * is the first "next sibling pointer" of the list .*/

10 ppwndNext = &pwndParent->pwndChiId;
11 while(*ppwndNext != NULL)
12  ppwndNext = &(*ppwndNext)->pwndSibling;
13
14 *ppwndNext = pwndNewBorn;
15}

这样,你就无需处理特殊路径了,减少了出错的可能。

6.5.Avoid using nested ?: operators.
避免使用嵌套的?:操作符。

【注】看下下面这段代码:

 1/* uCycleCheckBox -- return the next state for a checkbox.
 2*
 3* Given the current setting .uCur, return what the next
 4* checkbox state should be. This function handles both
 5* two-state checkboxes that toggle between 0 and 1, and
 6* three-state checkboxes that cycle through 2, 3, 4, 2, 
 7*/

 8unsigned uCycleCheckBox(unsigned uCur)
 9{
10 return ((uCur<=l) ? (uCur?O:l) : (uCur==4)?2:(uCur+l)):
11}

其中用到了嵌入的?:操作符。相当于以下用if语句的代码:

 1unsigned uCycleCheckBox(unsigned uCur)
 2{
 3 unsigned uRet ;
 4 if(uCur <= 1)
 5 {
 6  if (uCur != 0/* Handle the 0, 1, 0,. . . cycle. */
 7   uRet = 0;
 8  else
 9   uRet = 1;
10 }

11 else
12 {
13  if (uCur == 4 ) /* Handle the 2, 3, 4. 2.  cycle. */
14   uRet = 2;
15  else
16   uRet = uCur+l;
17 }

18 return (uRet);
19}

如果你的编译器优化的话,可能变成以下:

 1unsigned uCycleCheckBox(unsigned uCur)
 2{
 3 unsigned uRet ;
 4 if(uCur <= 1)
 5 {
 6  uRet = 0/* Handle the 0, 1, 0.  cycle. */
 7  if (uCur == 0)
 8   uRet = 1;
 9 }

10 else
11 {
12  uRet = 2/* Handle the 2 , 3, 4. 2.  cycle. */
13  if (uCur != 4)
14   uRet = uCur+l;
15 }

16 return(uRet);
17}

以上单个版本的代码不好理解,路径多。直接的实现如下:

1unsigned uCycleCheckBox(unsigned uCur)
2{
3 ASSERT(uCur >= 0 && uCur <= 4);
4 if(uCur == 1/* Time t o r e s t a r t the f i r s t cycle? */
5  return ( 0 ) ;
6 if(uCur == 4/* What about the second one? */
7  return ( 2 ) ;
8 return (uCur+l); /* Nope, nothing special t h i s time. */
9}

或者使用表来解决:

1unsigned uCycleCheckBox(unsigned uCur)
2{
3 static const unsigned uNextState[] = 10342 };
4 ASSERT(uCur >= 0 && uCur <= 4);
5 return (uNextState[uCur]);
6}

6.6.Handle your special cases just once.
只处理你的特殊情形一次。

【注】看看下面的代码:

1void *memchr(void *pv, unsigned char ch, size_t s i z e )
2{
3 unsigned char *pch = (unsigned char *)pv;
4 unsigned char *pchEnd = pch + size;
5 while (pch < pchEnd && *pch != ch)
6  pch++;
7 return ((pch < pchEnd) ? pch : NULL);
8}

其中对特殊情况有两处处理代码,这不利于维护。可以将同样特殊处理的代码合并在一处:

 1void *memchr(void *pv, unsigned char ch, size_t size )
 2{
 3 unsigned char *pch = (unsigned char *)pv;
 4 unsigned char *pchEnd = pch + size;
 5 while(pch < pchEnd)
 6 {
 7 if (*pch == ch)
 8 return (pch);
 9 pch++;
10 }

11 return(NULL);
12}

6.7.Avoid risky language idioms.
避免带风险的语言惯用法。

【注】再看看上面memchr的代码,它们都存在一个难以发现bug。这个地方:

1pchEnd = pch + size;
2while (pch < pchEnd)
3

如果要搜寻的地址范围正好是可寻址内存的最后一段,比如,pv指向可寻址内存范围的最后72 bytes,size=72,那么会发生什么?死循环。为了避免pchEnd指向一不存在的内存,一个可能的改进如下:

1pchEnd = pch + size - 1;
2while (pch <= pchEnd)
3

这样保证了pchEnd指向最后一个char的位置,这个位置肯定是存在的。
但是,这里又有一个新问题,前面也出过的overflow问题。(UCHAR-MAX overflow bug we saw earlier in BuildToLowerTable)当有指针和计数器时,安全的覆盖一个范围的方法是使用计数器来控制逻辑:

 1void *memchr(void *pv, unsigned char ch, size_t size )
 2{
 3 unsigned char *pch = (unsigned char *)pv;
 4 while (size-- > 0 )
 5 {
 6  if (*pch == ch)
 7  return (pch) ;
 8  pch ++;
 9 }

10 return (NULL);
11}

有人可能推荐你说使用 --size >= 0 代替 size-- > 0
理由是可以产生更高效的代码。问题是这样做是有可能会产生bug的,比如当size是unsigned值的时候,这个循环会永远执行下去。即使不是unsigned值,当size的初始值是INT_MIN的时候,--size会发生underflow。

另一个有风险的惯用法属于“未有效利用的提高效率方法”是使用位操作进行乘法除法以及2的次方数的mod操作。比如说在第二章中提到的快速版的memset函数中的代码:

1pb = (byte *)longfill((long *)pb, 1 , size / 4);
2size = size % 4;

有人可能会说怎么不优化成如下这样?

1pb = (byte *)longfill((long *)pb, 1 , size >> 2);
2size = size & 3;

问题是>>2等价于/4只在目标操作数是unsigned的value才成立。
C中还有很多带风险的习惯用法,最好的方式是你从你的bug中学习,然后并牢记那些带风险的习惯用法,从而去避免它们。

6.8.Don't needlessly mix operator types. If you must mix operators, use parentheses to
isolate the operations.
在不必要的时候不要混合使用操作符。如果必须这么做的话,使用括号来分隔这些符号。