【问题标题】:assembly code for C++ scoped static initializationC++ 范围静态初始化的汇编代码
【发布时间】:2014-02-27 16:12:42
【问题描述】:

我从

阅读了一些关于局部范围静态变量初始化顺序问题的旧文章

C++ scoped static initialization is not thread-safe 早在 2004 年,并且

Function Static Variables in Multi-Threaded Environments 2006 年。

然后我开始生成一个示例并检查我的编译器 gcc 4.4.7

int calcSomething(){}

void foo(){
    static int x = calcSomething();
}

int main(){
    foo();
    return 0;
}

objdump 的结果显示:

000000000040061a <_Z3foov>:
  40061a:   55                      push   %rbp
  40061b:   48 89 e5                mov    %rsp,%rbp
  40061e:   b8 d0 0a 60 00          mov    $0x600ad0,%eax
  400623:   0f b6 00                movzbl (%rax),%eax
  400626:   84 c0                   test   %al,%al
  400628:   75 28                   jne    400652 <_Z3foov+0x38>
  40062a:   bf d0 0a 60 00          mov    $0x600ad0,%edi
  40062f:   e8 bc fe ff ff          callq  4004f0 <__cxa_guard_acquire@plt>
  400634:   85 c0                   test   %eax,%eax
  400636:   0f 95 c0                setne  %al
  400639:   84 c0                   test   %al,%al
  40063b:   74 15                   je     400652 <_Z3foov+0x38>
  40063d:   e8 d2 ff ff ff          callq  400614 <_Z13calcSomethingv>
  400642:   89 05 90 04 20 00       mov    %eax,0x200490(%rip)        # 600ad8 <_ZZ3foovE1x>
  400648:   bf d0 0a 60 00          mov    $0x600ad0,%edi
  40064d:   e8 be fe ff ff          callq  400510 <__cxa_guard_release@plt>
  400652:   c9                      leaveq 
  400653:   c3                      retq   

不幸的是,我对汇编代码的了解非常有限,以至于我不知道编译器在这里做了什么。谁能告诉我,这个汇编代码是做什么的?它仍然不是线程安全的吗?我真的很感谢一些“伪代码”显示 gcc 在这里做什么。

EDIT-1: 正如 Jerry 所说,我启用了 O2 优化,汇编代码是:

0000000000400620 <_Z3foov>:
  400620:   48 83 ec 08             sub    $0x8,%rsp
  400624:   80 3d 85 04 20 00 00    cmpb   $0x0,0x200485(%rip)        # 600ab0 <_ZGVZ3foovE1x>
  40062b:   74 0b                   je     400638 <_Z3foov+0x18>
  40062d:   48 83 c4 08             add    $0x8,%rsp
  400631:   c3                      retq   
  400632:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
  400638:   bf b0 0a 60 00          mov    $0x600ab0,%edi
  40063d:   e8 9e fe ff ff          callq  4004e0 <__cxa_guard_acquire@plt>
  400642:   85 c0                   test   %eax,%eax
  400644:   74 e7                   je     40062d <_Z3foov+0xd>
  400646:   c7 05 68 04 20 00 00    movl   $0x0,0x200468(%rip)        # 600ab8 <_ZZ3foovE1x>
  40064d:   00 00 00 
  400650:   bf b0 0a 60 00          mov    $0x600ab0,%edi
  400655:   48 83 c4 08             add    $0x8,%rsp
  400659:   e9 a2 fe ff ff          jmpq   400500 <__cxa_guard_release@plt>
  40065e:   66 90                   xchg   %ax,%ax

【问题讨论】:

  • 看起来你编译时没有优化,这会导致一些糟糕的代码,但至少乍一看,它看起来像是线程安全的——__cxa_guard_acquire 看起来像获取互斥体,因此只有一个线程会调用calcSomething

标签: c++ multithreading gcc assembly static-variables


【解决方案1】:

是的。在伪代码中(对于未优化的情况),它类似于:

if (flag_val() != 0) goto done;
if (guard_acquire() != 0) goto done;
x = calcSomething();
guard_release_and_set_flag();
// Note releasing the guard lock causes later 
// calls to flag_val() to return non-zero.
done: return

flag_val() 实际上是一个非阻塞检查,显然是为了提高效率,除非必要,否则避免调用 acquire 原语。该标志必须由guard_release 设置,如图所示。 acquire 似乎是获取锁的同步调用。只有一个线程会返回一个真值并执行初始化。释放锁后,非零标志会阻止对锁的任何进一步接触。

另一个有趣的花絮是保护数据结构与静态内存中x 本身的值相距8 个字节。

那些熟悉带有内置线程的语言中的单例模式的人,例如Java 会识别这一点!

加法

现在还有一点时间,所以更详细一点:

000000000040061a <_Z3foov>:

  ; Prepare to access stack variables (never used in un-optimized code).
  40061a:   55                      push   %rbp
  40061b:   48 89 e5                mov    %rsp,%rbp

  ; Test a byte 8 away from the static int x. This is apparently an "initialized" flag.
  40061e:   b8 d0 0a 60 00          mov    $0x600ad0,%eax
  400623:   0f b6 00                movzbl (%rax),%eax
  400626:   84 c0                   test   %al,%al

  ; Goto the end of the function if the byte was no-zero.
  400628:   75 28                   jne    400652 <_Z3foov+0x38>

  ; Load the same byte address in di: the argument for the call to 
  ; acquire the guard lock. 
  40062a:   bf d0 0a 60 00          mov    $0x600ad0,%edi
  40062f:   e8 bc fe ff ff          callq  4004f0 <__cxa_guard_acquire@plt>

  ; Test the return value. Goto end of function if not zero (non-optimized code).
  400634:   85 c0                   test   %eax,%eax
  400636:   0f 95 c0                setne  %al
  400639:   84 c0                   test   %al,%al
  40063b:   74 15                   je     400652 <_Z3foov+0x38>

  ; Call the user's initialization function and move result into x.
  40063d:   e8 d2 ff ff ff          callq  400614 <_Z13calcSomethingv>
  400642:   89 05 90 04 20 00       mov    %eax,0x200490(%rip)        # 600ad8 <_ZZ3foovE1x>

  ; Load the guard byte's address again and call the release routine.
  ; This must set the flag to non-zero.
  400648:   bf d0 0a 60 00          mov    $0x600ad0,%edi
  40064d:   e8 be fe ff ff          callq  400510 <__cxa_guard_release@plt>

  ; Restore state and return.
  400652:   c9                      leaveq 
  400653:   c3                      retq   

This listing,虽然对于 LLVM 编译器而不是 g++(你在运行 OS X 吗?OS X 将 g++ 别名为 LLVM),但同意上面的猜测。 set_initialized 例程正在 guard_release 中设置标志值。

【讨论】:

  • 您的伪代码不可能完全准确,因为显然您必须将guard_val 设置为!= 0。从程序集(啊 AT&T 语法)我猜这实际上是在@987654332 中完成的@ 获取在edi 中传递的位置。这看起来很奇怪,但由于我们没有在函数的任何地方写信给600ab0,这似乎是唯一合乎逻辑的解释(实际上我们似乎将它传递给获取和释放,但显然它应该只在发布后设置)。
  • 公平点。我假设它隐含在获取锁中。我会摆弄文字。
  • 嗨,吉恩,感谢您的详细回答。我确实从中学到了很多。我假设线程将在guard_acquire() 处被抢占或阻塞,这保证了初始化只发生一次。但是关于guard_val(),从您的伪代码中,获取非零值的线程将返回。不是它将返回/使用/继续使用非初始化值的问题。或者这个线程是否也在guard_val() 处被抢占或阻塞?我看不出这如何防止发生未初始化的情况。
  • @pepero 谢谢。你说的对!必须有一个独立于锁值的标志。设置标志的必须是guard_release。我编辑了答案。我查看了 gcc c++ 库源代码,但没有时间找到获取和发布的原始代码。看看会很有趣。
猜你喜欢
  • 2013-07-21
  • 1970-01-01
  • 2016-01-09
  • 2021-10-02
  • 1970-01-01
  • 1970-01-01
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多