对《神奇的C语言》文中例子 5 代码的分析讨论

简介:   在春节前,我曾经参与在《神奇的C语言》一文中的例子(5)的讨论,但限于评论内容的有限,现在本文再次对这个问题单独讨论。(此问题原貌,详见《神奇的C语言》,这里我将原文中的代码稍做轻微改动,并重新给出如下)   原问题给出如下代码:   #include void func1(char a[]) {  //这里的参数 a 为指向数组的指针,因此 &a 和 a 的意义不同(前者为指针变量的地址,后者为指针变量的值)  //&a 表示指针变量的地址。

  在春节前,我曾经参与在《神奇的C语言》一文中的例子(5)的讨论,但限于评论内容的有限,现在本文再次对这个问题单独讨论。(此问题原貌,详见《神奇的C语言》,这里我将原文中的代码稍做轻微改动,并重新给出如下)

  原问题给出如下代码:

 

#include <stdio.h>
void func1(char a[])
{

  //这里的参数 a 为指向数组的指针,因此 &a 和 a 的意义不同(前者为指针变量的地址,后者为指针变量的值)
  //&a 表示指针变量的地址。
  //&a[0] 等效为 a ,即指针变量的值。
_tprintf(_T(
"func1: &a = 0x%08X; &a[0] = 0x%08X;\n"), &a, &a[0]); } int _tmain(int argc, _TCHAR* argv[]) { char a[10];

  //这里的 a 是数组名,相当于字面地址,所以 &a 相当于直接写成 a 。 _tprintf(_T(
"wmain: &a = 0x%08X; &a[0] = 0x%08X;\n"), &a, &a[0]);

   //数组名作为参数传递给其他函数时,退化为指针 func1(a);
return 0; }

 

  以 VS2005 编译,采用默认项目配置(Unicode 编码),在 Release 版本的输出结果如下(可见 func1 中的 &a 和其他输出不同,且相差 4 ,在 debug 版本下此差值是一个较大的数值):

 

  ----------------------------------------------------

  Output:

  ----------------------------------------------------

  wmain: &a = 0x0018FF38; &a[0] = 0x0018FF38;

  func1 : &a = 0x0018FF34; &a[0] = 0x0018FF38;

  ----------------------------------------------------

 

  以 IDA 反汇编 Release 版本的可执行文件,得到 wmain 函数的汇编代码如下:

wmain   proc near
        
var_14  = dword ptr -14h        ; func1 的实际参数(char* a)
var_10  = dword ptr -10h        ; a 的起始地址
var_4   = dword ptr -4          ; 用于 ESP 校验

sub     esp, 14h                ; 为临时变量分配空间
mov     eax, __security_cookie
xor     eax, esp
mov     [esp+14h+var_4], eax    ; 保存 ( ESP ^ _security_cookie ) 到 var_4
push    esi
mov     esi, ds:__imp__wprintf
lea     eax, [esp+18h+var_10]   ; wmain: &a[0] (0018FF38)
push    eax
mov     ecx, eax                ; wmain: &a (0018FF38)
push    ecx
push    offset pStr1            ; 字符串 "wmain: &a = 0x%08X; &a[0] = 0x%08X;\n"
call    esi                     ; __imp__wprintf 打印输出
lea     edx, [esp+24h+var_10]   ; func1: &a[0] (0018FF38)
mov     eax, edx
push    eax
lea     ecx, [esp+28h+var_14]   ; func1: &a (0018FF34), 参数的地址
push    ecx
push    offset pStr2            ; 字符串 "func1: &a = 0x%08X; &a[0] = 0x%08X;\n"
mov     [esp+30h+var_14], edx   ; 为实际参数赋值
call    esi                     ; __imp__wprintf 打印输出
mov     ecx, [esp+30h+var_4]
add     esp, 18h                ; 为以上两次 _tprintf 函数调用复原栈指针
pop     esi
xor     ecx, esp
xor     eax, eax
call    __security_check_cookie ; 检查 ESP 是否被意外破坏
add     esp, 14h                ; 释放栈上的临时变量空间
retn

 

  从以上汇编代码,可以得到关于 Release 版本代码(以优化运行效率为主要目标)的如下结论:

 

  (1)直接使用 ESP 寻址函数内的临时变量或参数。

  (2)func1 函数调用被编译器直接内联到 wmain 函数体内。在内联 func1 时,编译器对代码做了等效性变换,代码和栈上数据的顺序,与通常函数调用相比有细微差别,但运行结果是等效的。

  (3)在寄存器保护环节,保存了 ESI (目标索引)寄存器,其用意是以 ESI 加载 __imp__wprintf 的运行时(绑定后)地址。(对于默认配置,此函数是来自 VS2005 运行时库 msvcr80.dll 中的导入函数,函数地址位于导入表中,在加载时被绑定)

 

  下面是根据以上汇编代码得到的 wmain 函数的栈上数据示意图(图中栈的增长方向为从下向上,并已经根据 输出结果 推算出了栈上的虚拟地址):

 

  

 

  上面的表格中包括了两次对 __imp__wprintf 调用时的参数,其中 __imp__wprintf 的栈帧,除了参数之外的其余部分在表中没有显示,即可以认为上表是第二次 __imp_wprintf 已返回到 wmain 函数时的栈上数据快照,两次函数调用的复原栈指针(即释放参数占用的空间)被合并为一条指令(add esp, 18h)。表格中的红色数据,即为代码中交由 _tprintf 打印输出的值。其中 pStr1 和 pStr2 指向位于 .rdata section(只读数据段)上的字符串(根据项目选项,为 Unicode 编码)。

 

  其中 ESP 校验过程为,在 wmain 函数的起始位置,为临时变量分配空间后,将此时的 ESP 和一个特定常数(_security_cookie)异或,结果保存到 wmain 的第一个临时变量(var1)中,之后调用了 __imp__wprintf 等其他函数后,把 ESP 和 var1 异或的结果保存到 ECX 中(此时 ECX 的期待值为 _security_cookie),然后检测 ECX 和 _security_cookie 是否相等即可。

 

  【注】:表格中的栈指针校验值,根据汇编代码可以看出,相当于首个出现的函数临时变量,它的值的意义是,为临时变量分配空间后 (T1 时刻),将此时的 ESP 和一个常量值异或,存储于该临时变量。在复原栈指针之前(T2 时刻),校验 ESP 是否吻合 T1 时刻的值。 -- hoodlum1980,2014-4-11

 

  综合以上图表,对代码输出则可以比较容易做出解释:

 

  第一行输出结果为 wmain 函数中的 &a 和 a (a 为数组名)在写法上等效的体现,在 wmain 里 a 为本地数组的数组名(这里”本地“的含义指的是对其声明的可见性),如果把 a 理解为数组,&a 表示数组的存储地址,如果把 a 理解为相当于数组元素指针,则 &a 不具有实际物理意义,因此 &a 和 &a[0] 都等效于 a,即数组的起始地址。

 

  第二行输出结果为 func1 函数中的 &a 和 a (a 为指针变量)在意义上不同的体现,a 是一个指向数组的指针变量(以及 func1 的实际参数),&a 表示此指针变量的地址,&a[0] 表示被指向数组的起始地址,即 &a[0] =  a + 0 * sizeof (char) = a (这里为数学计算含义),  即指针变量 a 的值。在本例输出中,func1 的实际参数 a 与”数组起始地址“紧邻,a 的地址为 0018FF34h,a 的值为 0018FF38h(指向 wmain 中的数组)。

 

  因此,本范例的代码,可以认为在原理上即相当于如下代码:

 

int _tmain(int argc, _TCHAR* argv[])
{
    //main 中的结果:
    char a[10];
    _tprintf(_T("main_: &a = 0x%08X; &a[0] = 0x%08X;\n"), a, a);

    //func1 中的结果
    char *p = a;
    _tprintf(_T("func1: &a = 0x%08X; &a[0] = 0x%08X;\n"), &p, p);

    return 0;
}

 

  【附】

  本文中引用的范例来自于:《神奇的C语言》 中的例子 5 ,http://www.cnblogs.com/linxr/p/3521788.html

 

目录
相关文章
|
3月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
130 1
|
3月前
|
存储 C语言
【C语言】基础刷题训练4(含全面分析和代码改进示例)
【C语言】基础刷题训练4(含全面分析和代码改进示例)
|
1月前
|
存储 搜索推荐 C语言
深入C语言指针,使代码更加灵活(二)
深入C语言指针,使代码更加灵活(二)
|
1月前
|
存储 程序员 编译器
深入C语言指针,使代码更加灵活(一)
深入C语言指针,使代码更加灵活(一)
|
1月前
|
C语言
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
|
2月前
|
安全 C语言
在C语言中,正确使用运算符能提升代码的可读性和效率
在C语言中,运算符的使用需要注意优先级、结合性、自增自减的形式、逻辑运算的短路特性、位运算的类型、条件运算的可读性、类型转换以及使用括号来明确运算顺序。掌握这些注意事项可以帮助编写出更安全和高效的代码。
50 4
|
1月前
|
C语言
C语言练习题代码
C语言练习题代码
|
2月前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
567 6
|
2月前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
207 5
|
2月前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
397 3
下一篇
无影云桌面