美文网首页
从汇编角度来看 *ptr++ 的底层实现

从汇编角度来看 *ptr++ 的底层实现

作者: 廖少少 | 来源:发表于2019-12-10 23:43 被阅读0次

    引言

    最近看到*ptr++,不是很理解,遂上StackOverflow查询,找到一个帖子:https://stackoverflow.com/questions/859770/post-increment-on-a-dereferenced-pointer
    其中有人给出了*ptr++背后的原理:

    1. 保存当前的ptr
    2. 自增ptr
    3. 对步骤1保存的ptr进行解引用,此时得到表达式的最终值

    于是,对于*ptr++可以等价理解为下面的代码:

    *ptr;
    ptr = ptr + 1; 
    

    顺便复习一下C++操作符的优先级:

    c++operatorpriority.png

    初探普通指针

    举个例子,考虑下面代码:

    int main()
    {
        int i = 5, *ptr = &i;
        cout << *ptr++ << endl;;
        cout << *ptr << endl;
    }
    

    结果为:

    5
    2293320
    

    因为ptr是一个指针,它存储指向的对象的地址,令该地址+1,再获取这个地址的对象的值,因为我们并没有在这个地址存储对象,所以别指望输出什么有效的信息

    那如果用括号括起来呢?

    int main()
    {
        int i = 5, *ptr = &i;
        cout << *(ptr++) << endl;;
    }
    

    结果还是:

    5
    2293320
    

    继续,考虑*++ptr++*ptr

    int main()
    {
        int i = 5, *ptr1 = &i, *ptr2 = &i;
        cout << *++ptr1 << endl;
        cout << ++*ptr2 << endl;
        cout << i << endl;
    }
    

    结果为:

    2293308
    6
    6
    

    可以看到,*++ptr1遵循从右往左的结合律(associativity),先对ptr1进行自增,然后再解引用,这里我们没有存储对象。++*ptr2同样遵循从右往左的结合律,先对ptr2进行解引用,即得到变量i,再对i进行自增,得到6。于是,最后一行输出变量i时,输出为6。

    再探迭代器

    迭代器是STL中经常使用的,我们来看看迭代器的解引用与自增操作是否与普通指针一样:

    int main()
    {
        vector<int> vec = {10, 20, 30, 40};
        auto it = vec.begin();
        cout << *it << endl;
        cout << *it++ << endl;
        cout << *it << endl;
        cout << *++it << endl;
        cout << *it << endl;
        cout << ++*it << endl;
        cout << *it << endl;
    }
    

    结果如下,可以看到,迭代器与普通指针表现一致

    10
    10
    20
    30
    30
    31
    31
    

    深入普通指针的汇编实现

    *ptr的汇编实现

    正好最近看完了CSAPP这本书,虽然本科在微机原理中学过一些汇编,但那些应试教育学过就忘得差不多了,看完CSAPP后,我对汇编语言的价值有了新的认识,于是考虑用反汇编的手段详细探索一下*ptr++底层的汇编实现。

    我编写了两个cpp文件,

    //helloworld.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int i = 5, *ptr = &i;
    }
    
    //helloworld_copy.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int i = 5;
    }
    

    GCC可以帮我们得到汇编代码:

    $ gcc -c -S helloworld.cpp
    $ gcc -c -S helloworld_copy.cpp
    

    得到helloworld.shelloworld_copy .s这两个汇编文件,为了对比不同,可以用IDE支持的compare功能,这里我就用VSCODE的compare功能

    helloworldcompare.png

    主要看两个汇编文件不同的地方:

    • movl传送双字
      • helloworld_copy把立即数5($5)送到地址-4(%rbp)处,这样我们可以说变量i存储在-4(%rbp)
      • helloworld把立即数5($5)送到地址-12(%rbp)处,这样我们可以说变量i存储在-12(%rbp)
    • leaq取出有效地址,注意它不会引用内存,而仅仅是将有效地址写入到目的寄存器里面,在右边的第23行,就是把变量i的地址加载到寄存器rax中,于是rax相当于一个指向i的指针
    • movq传送四字,把寄存器rax的内容传送到地址-8%(rbp),别忘了rax存储的是变量i的地址,所以我们可以说指针ptr存储在地址-8%(rbp)
    • -12(%rbp)-8%(rbp)差了4,正好是int型变量在64位操作系统中所占空间大小

    如果为两个cpp文件中变量i输出到cout

    //helloworld.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int i = 5, *ptr = &i;
        cout << i;
    }
    
    //helloworld_copy.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int i = 5;
        cout << i;
    }
    

    compare汇编结果如下,输出到cout其实就是把变量i传送到寄存器eax中:

    coutcompare.png

    再看一看表达式*ptr的汇编实现,在helloworld.cpp中,cout输出*ptr,汇编代码如下:

        movl    $5, -12(%rbp)
        leaq    -12(%rbp), %rax
        movq    %rax, -8(%rbp)
        movq    -8(%rbp), %rax
        movl    (%rax), %eax
    

    (%rax)表示加载寄存器rax中地址所指的内容,这与C++代码一致,因为指针ptr本身存储在地址-8%(rbp),即存储在rax中,所以对指针ptr解引用,就相当于获得rax所指向的地址的内容。

    *ptr++的汇编实现

    我们对比看一看i++*ptr++的汇编实现,假设两个汇编文件各自输出这两个表达式,汇编代码如下:

    inccompare.png

    先看左边,这是输出i++的汇编实现,第24行是最迷惑的,我们可以反推一下

    • 第26行,cout输出的还是在第23行得到的寄存器eax中的值
    • 第24行,leal计算%rax+1然后保存在edx
    • 第25行,edx中的值应该是6,于是反推第24行的%rax+1应该是6,于是rax中的值应该是5,而前文并没有出现rax所以猜想rax就是与地址-4(%rbp)所存储内容相同(如果有错麻烦告知一下)

    再看右边,这是输出*ptr++的汇编实现

    • 第22行,变量i的值为5,存储在-12(%rbp)
    • 第23行,将变量i的地址写入rax
    • 第24行,将rax的值写入地址-8(%rbp)中,这个就是指针ptr的所在之处
    • 第26行,将%rax+4的结果写入rdx,因为rax中的值就是变量i的地址,所以相当于把地址+4后写入rdx,那为什么指针+1会导致地址+4?我的环境是64位操作系统,一个int*型变量应该是8个字节,好像应该是地址+8才对?但是这个地址存储的是int型变量,在64位OS中占4字节,所以指针+1就是地址+4,没毛病
    • 第27行,将rdx的值写入地址-8(%rbp),这是指针ptr得到更新后的值
    • 第29行,cout输出的是第28行寄存器rax所指的地址的值,而rax是在第25行得到,这在第26自行增操作之前,所以输出仍为5

    总结:以上分析正好与StackOverflow中的回答相一致,*ptr++先存储指针,再让指针加一,最后再解引用之前存储的那个指针,表达式最后的结果是指针原来所指对象

    ++*ptr的汇编实现

    接下来看一看++*ptr的汇编实现,为了对比,helloworld_copy.cppcout输出的是++i

    preinccompare.png

    左边的++i的汇编实现是显而易见的,用了addl把立即数1加到i上面,最后输出到cout

    右边的++*ptr的汇编实现有点繁琐,我们一步步来看

    • 第22-25行与前文一致,变量i存储在-12(%rbp),指针ptr存储在地址-8%(rbp),即rax
    • 第26行,把rax所指地址的值写入eax,这时eax中的值为5,这里有个知识点不能忽略:eaxrax的低32位寄存器,int型变量在64位OS中占4字节,所以推测rax的值也变成5
    • 第27行,将%rax+1的结果写入edx,推测成立,此时edx的值为6
    • 第28行,将地址-8(%rbp)中的值写入rax中,所以rax存储变量i的地址,此时rax又变回指针ptr
    • 第29行,将edx中的值写入到rax所指向的地址中,即写入指针ptr中,至此,变量i获得自增
    • 第30-32,将地址-8%(rbp)所存内容输出到cout

    反推确认一下,我们已知最后输出的是6,所以第31行eax为6, 所以第30行rax所指的地址的值为6, 第30行地址-8%(rbp)所存储的是一个地址,地址中的值为6,第29行更新了rax所指向的地址的值,这也就更新了地址-8%(rbp)所指向的地址的值,所以反推得到第29行的edx的值为6,而edx来自于第27行的leal操作,所以leal操作就是自增操作

    总结:++*ptr先解引用指针,获取指针所指的对象,再令该对象+1,表达式最后的结果是自增后的对象

    *++ptr的汇编实现

    仿照前面的操作,不同的是helloworld.cppcout输出的是*++ptr

    starincptr.png

    直接看右边,第22-24与前文一致,第25行将立即数4加在了地址-8(%rbp)中的值上,从第23、24行可知地址-8(%rbp)中本来存的是地址-12(%rbp),于是这就相当于地址+4,第26、27行就是解引用这个加4后的地址,明显这是个无效地址,所以输出的是2293320这种毫无意义的值

    总结:*++ptr先自增指针,然后解引用指针获取指针所指的对象,表达式最后的结果是指针自增后所指的对象

    关于自增的小tips

    it++++it均可的情况下,用++it,可提高非常微小的性能

    it++ returns a copy of the previous iterator. Since this iterator is not used, this is wasteful. ++it returns a reference to the incremented iterator, avoiding the copy.

    相关文章

      网友评论

          本文标题:从汇编角度来看 *ptr++ 的底层实现

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