64.8 使用指针的函数参数

…更有意思的是,有可能在程序中,取一个函数参数的指针并将其传递给另外一个函数。

  1. #include <stdio.h>
  2. // located in some other file
  3. void modify_a (int *a);
  4. void f (int a)
  5. {
  6. modify_a (&a);
  7. printf ("%d\n", a);
  8. };

很难理解它是如果实现的,直到我们看到它的反汇编码:

Listing 64.10: Optimizing MSVC 2010

  1. $SG2796 DB '%d', 0aH, 00H
  2. _a$ = 8
  3. _f PROC
  4. lea eax, DWORD PTR _a$[esp-4] ; just get the address of value in local stack
  5. push eax ; and pass it to modify_a()
  6. call _modify_a
  7. mov ecx, DWORD PTR _a$[esp] ; reload it from the local stack
  8. push ecx ; and pass it to printf()
  9. push OFFSET $SG2796 ; '%d'
  10. call _printf
  11. add esp, 12
  12. ret 0
  13. _f ENDP

传递到另一个函数是a在栈空间上的地址,该函数修改了指针指向的值然后再调用printf()来打印出修改之后的值。

细心的读者可能会问,使用寄存器传参的调用约定是如何传递函数指针参数的?

这是一种利用了影子空间的情况,输入的参数值先从寄存器复制到局部栈中的影子空间,然后再讲这个地址传递给其他函数。

Listing 64.11: Optimizing MSVC 2012 x64

  1. $SG2994 DB '%d', 0aH, 00H
  2. a$ = 48
  3. f PROC
  4. mov DWORD PTR [rsp+8], ecx ; save input value in Shadow Space
  5. sub rsp, 40
  6. lea rcx, QWORD PTR a$[rsp] ; get address of value and pass it to modify_a()
  7. call modify_a
  8. mov edx, DWORD PTR a$[rsp] ; reload value from Shadow Space and pass it to printf()
  9. lea rcx, OFFSET FLAT:$SG2994 ; '%d'
  10. call printf
  11. add rsp, 40
  12. ret 0
  13. f ENDP

GCC同样将传入的参数存储在本地栈空间:

Listing 64.12: Optimizing GCC 4.9.1 x64

  1. .LC0:
  2. .string "%d\n"
  3. f:
  4. sub rsp, 24
  5. mov DWORD PTR [rsp+12], edi ; store input value to the local stack
  6. lea rdi, [rsp+12] ; take an address of the value and pass it to modify_a()
  7. call modify_a
  8. mov edx, DWORD PTR [rsp+12] ; reload value from the local stack and pass it to printf()
  9. mov esi, OFFSET FLAT:.LC0 ; '%d'
  10. mov edi, 1
  11. xor eax, eax
  12. call __printf_chk
  13. add rsp, 24
  14. ret

ARM64的GCC也做了同样的事情,但这个空间称为寄存器保护区:

  1. f:
  2. stp x29, x30, [sp, -32]!
  3. add x29, sp, 0 ; setup FP
  4. add x1, x29, 32 ; calculate address of variable in Register Save Area
  5. str w0, [x1,-4]! ; store input value there
  6. mov x0, x1 ; pass address of variable to the modify_a()
  7. bl modify_a
  8. ldr w1, [x29,28] ; load value from the variable and pass it to printf()
  9. adrp x0, .LC0 ; '%d'
  10. add x0, x0, :lo12:.LC0
  11. bl printf ; call printf()
  12. ldp x29, x30, [sp], 32
  13. ret
  14. .LC0:
  15. .string "%d\n"

顺便提一下,一个类似影子空间的使用在这里也被提及过(46.1.2)。 #65章 线程局部存储

TLS是每个线程特有的数据区域,每个线程可以把自己需要的数据存储在这里。一个著名的例子是C标准的全局变量errno。多个线程可以同时使用errno获取返回的错误码,如果是全局变量它是无法在多线程环境下正常工作的。因此errno必须保存在TLS。

C++11标准里面新添加了一个thread_local修饰符,标明每个线程都属于自己版本的变量。它可以被初始化并位于TLS中。

Listing 65.1: C++11

  1. #include <iostream>
  2. #include <thread>
  3. thread_local int tmp=3;
  4. int main()
  5. {
  6. std::cout << tmp << std::endl;
  7. };

使用MinGW GCC 4.8.1而不是MSVC2012编译。

如果我们查看它的PE文件,可以看到tmp变量被放到TLS section。