- UID
- 418
- 精华
- 积分
- 3994
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
楼主 |
发表于 2017-4-5 13:23:58
|
显示全部楼层
二、更复杂的++--表达式以及更好玩的内容
这一“劫”开始之前,我先总结一下上一节的内容。前置++运算符将被操作变量递增后赋值给后续内容。后置++运算符先将原值赋值后再进行递增。简单来记忆:++在前先加,++在后后加。‘--’类似,只不过把结果递减一。
这次我们来看看很多个++的情况:还是在环境1中我们分别编译以下2组代码。
[X组]
#include <stdio.h>
int main()
{
int b = 1;
b = b+++++b+++b;
return 0;
}
[Y组]
#include <stdio.h>
int main()
{
int b = 1;
b = b ++ + ++ b + ++ b;
return 0;
}
结果如下:
X组:不能被编译,语法错误。编译器给出Error:C2105 ‘++’ needs l-value. (++需要左值。)
Y组:b == 10;
如果你没法预测Y组中变量b的结果,没关系。我们将Y组中的代码原封不动,换个平台编译。我们将在作业系统为OSX EI Capitan 10.11开发环境为Xcode 8.2测试Y组代码。我们将这个环境称为:“环境2”。所以环境2中的编译器前端为 Clang 800.0 后端为 Apple LLVM 8.0。
编译执行后的结果为:b == 8;
我们先来解释为什么X组会通不过编译。换个思路的话,如果你是编译器,你怎样将 b = b+++++b+++b; 这句表达式进行分词呢?(编译器前端的简介请参见帖子:lex与yacc简介 https://www.0xaa55.com/forum.php ... &extra=page%3D2 )如果你将如上语句看做:b = (b++) + (++b) + (++b); 的话也许会通过编译。但是所有的C编译器都不会这样理解。而恰恰,C编译器将如上语句看做 b = b ++ ++ + b ++ + b; 因为C编译器在词法分析的时候进行“Greedy Matching 贪婪匹配”。也就是说能多加一个字符构成合法标识符绝对不会少加那个字符。下面我们像词法分析器那样逐字扫描表达式“b = b+++++b+++b;”
“b = b+++++b+++b;” 1.’b’=>b开头的某种标识符。
“b = b+++++b+++b;” 2.’b空格’=>确定为标识符b。
“b = b+++++b+++b;” 3.‘=’=>以等号开始的某种运算符,可能是‘==’也可能是‘=’。
“b = b+++++b+++b;” 4.’=空格’=>确定为赋值运算符,将该词素传给语法分析器。
“b = b+++++b+++b;” 5.’b’=>b开头的某种标识符。
“b = b+++++b+++b;” 6.’b+’=>确定为标识符b。并将b传递给语法分析器。词法分析器保留扫描到的加号。
“b = b+++++b+++b;” 7.‘++’=>确定为++运算符。注意此时词法分析器无法确定这是后缀++。因为词法分析器不保留前面分析到的结果中有变量b。在语法分析器中++被识别为后置++。这一步中词法分析器仅做贪婪匹配,并将++词素传给Parser(语法分析器)。
“b = b+++++b+++b;” 8.‘+’=>加号开头的某种运算符,可能是自增加‘++’,也可能是双目加‘+’。
“b = b+++++b+++b;” 9.’++’=>确定为自增++,传递给Parser。
“b = b+++++b+++b;” 10.’+’=>加号开头的某种运算符,可能是自增加‘++’,也可能是双目加‘+’。
“b = b+++++b+++b;” 11.’+b’=>确定为双目加‘+’。扔给Parser。‘b’保留。识别为b开头的某种标识符。
“b = b+++++b+++b;” 12.’b+’=>确定为标识符b。扔给Parser词素‘b’。保留‘+’待定。
“b = b+++++b+++b;” 13.’++’=>确定为自增运算符‘++’。传给Parser。
“b = b+++++b+++b;” 14.‘+’=>加号开头的某种运算符,可能是自增加‘++’,也可能是双目加‘+’。
“b = b+++++b+++b;” 15.’+b’=>确定为双目加‘+’。扔给Parser。‘b’保留。识别为b开头的某种标识符。
“b = b+++++b+++b;” 16.’b;’=>确定为标识符b。扔给Parser词素‘b’。保留‘;’。
“b = b+++++b+++b;” 17.’;’=>语句结束。并告诉Parser语句结束。
上面,我们用了17步完成了绝大多数C编译器在对表达式“b = b+++++b+++b;”做词法分析时的过程。相信大家已经理解什么是贪婪匹配了。(Tips:贪婪匹配不单单发生在运算符++上面。所有标识符都是贪婪匹配的。当然语法分析器不存在什么贪婪匹配。如果有如下语句: struct b {int a; int b;} B = { 0 }, * PB = &B; 那么 PB空格->a = 1; 没有语法错误。PB-空格>a = 1; 就会出现语法错误。原因是扫描到空格的时候‘-’已经被送到了语法分析器当中。语法分析器是没有“贪婪匹配”这一说法的。语法分析器中,前面是双目减,当前是关系运算符大于。因为‘-’缺少右侧表达式当然出现语法错误。)
表达式“b = b+++++b+++b;”在词法分析的过程中被贪婪匹配为“b = b ++ ++ + b ++ + b;”。而根据 ANSI C 标准,后置递增和递减运算符必须拥有量化、非量化实型或者指针类型的具有可修改的左值。“b++”具有可以被修改的左值,而“b++ ++”中的后一个‘++’就缺少了可以被修改的左值。不难想象 Visual C++ 给出 C2105 错误。
接下来解决Y组在环境1和环境2中答案不一致的问题。
我们直接来看环境1下Y组的反汇编结果:
- ; int a = 0, b = 1;
- 1. mov dword ptr [a],0 ; 赋值,不解释。
- 2. mov dword ptr [b],1 ; 同上。
- ; b = b ++ + ++ b + ++ b;
- 3. mov eax,dword ptr [b] ; EAX = b;
- 4. add eax,1 ; EAX = EAX + 1;
- 5. mov dword ptr [b],eax ; b = EAX;
- 6. mov ecx,dword ptr [b] ; ECX = b;
- 7. add ecx,1 ; ECX = ECX + 1;
- 8. mov dword ptr [b],ecx ; b = ECX;
- 9. mov edx,dword ptr [b] ; EDX = b;
- 10. add edx,dword ptr [b] ; EDX = EDX + b;
- 11. add edx,dword ptr [b] ; EDX = EDX + b;
- 12. mov dword ptr [b],edx ; b = EDX;
- 13. mov eax,dword ptr [b] ; EAX = b;
- 14. add eax,1 ; EAX = EAX + 1;
- 15. mov dword ptr [b],eax ; b = EAX;
复制代码
把Y组代码在环境2中的反汇编结果拿来分析:
(首先要说明一个问题:前面说过“环境2”内OS为OSX 10.11.编译器是Clang。当然我的硬件包括一块Intel Core i7. 那么汇编代码如此不一样,差别在哪里?其实测试ABCD组和测试XY组时我使用的是同一块CPU。ABCD组在环境1中被反汇编为Intel语法的汇编语言。而在环境2中测试Y组程序被反汇编为AT&T语法的汇编语言。而且环境2是客户机上的真实CPU,总线宽度64位。环境1是在真实CPU上虚拟出来的x86环境,模拟了一块总线宽度为32位的CPU。在汇编语言层面上,Intel语法的汇编代码中,MOV EAX,EBX 表示将寄存器EBX中的值拷贝到寄存器EAX中。AT&T语法的汇编代码正好相反 movl %eax, %ebx 表示将寄存器EAX中的值拷贝到寄存器EBX中。‘%’符号是OSX平台上的‘gas(GNU Assembler:GNU汇编器)’为了区别Intel语法与AT&T语法而特有的标记。)
- 0x100000f76 <+6>: movl $0x0, -0x4(%rbp)
- 0x100000f7d <+13>: movl $0x1, -0x8(%rbp)
- 0x100000f84 <+20>: movl -0x8(%rbp), %ecx
- 0x100000f87 <+23>: movl %ecx, %edx
- 0x100000f89 <+25>: addl $0x1, %edx
- 0x100000f8c <+28>: movl %edx, -0x8(%rbp)
- 0x100000f8f <+31>: movl -0x8(%rbp), %edx
- 0x100000f92 <+34>: addl $0x1, %edx
- 0x100000f95 <+37>: movl %edx, -0x8(%rbp)
- 0x100000f98 <+40>: addl %edx, %ecx
- 0x100000f9a <+42>: movl -0x8(%rbp), %edx
- 0x100000f9d <+45>: addl $0x1, %edx
- 0x100000fa0 <+48>: movl %edx, -0x8(%rbp)
- 0x100000fa3 <+51>: addl %edx, %ecx
- 0x100000fa5 <+53>: movl %ecx, -0x8(%rbp)
复制代码
下面来逐行解释以上反汇编结果:
<+ 6> 1. a = 0;
<+13> 2. b = 1;
<+20> 3. ECX = b;
<+23> 4. EDX = ECX;
<+25> 5. EDX = EDX + 1;
<+28> 6. b = EDX;
<+31> 7. EDX = b;
<+34> 8. EDX = EDX + 1;
<+37> 9. b = EDX;
<+40> 10. ECX = ECX + EDX;
<+42> 11. EDX = b;
<+45> 12. EDX = EDX + 1;
<+48> 13. b = EDX;
<+51> 14. ECX = ECX + EDX;
<+53> 15. b = ECX;
汇编代码翻译完毕后,以上问题变成了与“后置++ 在原表达式中 哪一个序列节点上 进行评估 后缀++的结果”的等价问题。
在环境2的反汇编结果中,2~6行更新了一次b递增后的结果。并同时保存了 b 的初值到寄存器ECX中。对应后缀++的递增运算。7~9行再次更新了b递增后的结果。迭代初始值是b内存地址中的内容。保留更新结果在EDX中。对应第一个前置++运算。第10行将b在2~6行之前的初值1加上7~9行执行完毕后的结果。(我猜是为了保证后缀++运算没有影响到第10行。)对应第一个双目加法运算。11~13行又一次更新累加了b地址中的内容。这是第二次更新式的累加运算。应该对应第二个前置++运算符。14~15行将第二个双目加法运算的结果累加到最新的b地址中的内容中去,并完成计算。
我们来看原表达式“b = b++ + ++b + ++b;”,为了方便理解我们将原表达式做如下标记:
- b = (b++) + (++b) + (++b);
- f1 f2 f3 f4 f5 f6
复制代码
接着我们把f2这个后置++运算分解为累加部分 f2a 和更新部分 f2b. 那么环境2中编译结果相对应的运算顺序则是:f2a, f4, f3, f6, f5, f1. 在环境1的反汇编结果中,3~5行更新了一次b递增后的结果。6~8行再次更新了b递增后的结果。9~13行在前两次更新的基础上完成了两个双目加法运算。14~15行递增并更新了当前内存中的b。所以环境1中编译结果相对应的运算顺序则是:f4, f6, f5, f3, f2a, f1, f2b.
如果按照C语言运算符优先级顺序,加之从左到右的自上而下的分析,还有ANSI标准对副作用和序列节点的阐述,正确的运算执行序列应该是:f2a, f4, f6, f3, f5, f1, f2b. 首先因为前/后置++ - -拥有相同的运算优先级,具有从左到右的结合性。其优先级高于双目加法运算。所以 f2, f4, f6 依次运算。其次我们依次从左往右计算两个优先级相同的双目加法运算来避免它们之间存在的二义性冲突问题。所以f3与f5之间的执行顺序确定为 f3, f5. 最后执行完毕最低优先级的赋值运算后,根据帖子第一节所述的C语言的7种序列节点其中的第三条:“序列节点可以出现在一条完整表达式的末尾。”。我们将 f2b 放在最后运算。
我们可以看出,环境1, 2中编译结果的不同是因为二义性文法带来的严重混乱。环境2的执行序列中将f3与f6的优先级忽略,还忽略了f2b运算。环境1下虽然没忽略f2b运算,但是f3与f2a之间颠倒了优先级顺序。
扩展阅读:
1. Keyword:l-value。
2. Keyword:sequence point.
3. Keyword: Side-effect Computer science.
4. Keywords: Assembler, Assembly, GAS, AS.
5. 书籍:《C陷阱与缺陷》。
开心一刻:
我经常把“反汇编”读成“huan fei bian”。觉得搞笑,你也把上文中所有的“反汇编”读成“huan fei bian”试试。最后改不过来别怪我。
写这篇帖子历时5个小时,OSX 因为某些 APP 死机3次。死机时间比高达60%(times per hour)
在完成表达式“b = b+++++b+++b;”的词法分析过程后,你已经具备了人肉词法分析的资质,去找份专业对口的工作吧!
写帖子的5个小时内,我一边补番“约会大作战”一边“huan fei bian”。结果导致番剧一个字也没听到。以后要重新加补5个小时。 |
|