Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

用 gdb 查看汇编代码

Posted on 2022-06-29

肖像2018facebook.jpg

我们大多数人使用高级语言(Go、C++)编写代码,但是如果您想了解对您的处理器很重要的代码,您需要查看代码的“汇编”版本。组装只是一系列指令。

起初,汇编代码看起来令人生畏,我不鼓励您在汇编中编写大型程序。但是,只需很少的培训,您就可以学会计算指令和发现分支。它可以帮助您更深入地了解您的程序是如何工作的。让我来说明你可以通过查看汇编来学到什么。让我们考虑以下 C++ 代码:

长f ( int x ) {      长数组[ ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 999 , 10 } ;      返回数组[ x ] ;  }    长f2 ( int x ) {      长数组[ ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 999 , 10 } ;      返回数组[ x + 1 ] ;  }  

此代码包含两个 80 字节数组,但它们是相同的。这是担心的根源吗?如果您查看大多数编译器生成的汇编代码,您会发现完全相同的常量通常被“压缩”(仅存储一个版本)。如果我使用 gcc 或 clang 编译器使用 -S 标志编译这两个函数,我可以清楚地看到压缩,因为数组只出现一次:

 。文本  .文件“f.cpp”  .globl _Z1fi // -- 开始函数 _Z1fi  .p2对齐 2  .type _Z1fi, @function  _Z1fi: // @_Z1fi  .cfi_startproc  // %bb.0:  adrp x8, .L__const._Z2f2i.array  添加 x8, x8, :lo12:.L__const._Z2f2i.array  ldr x0, [x8, w0, sxtw #3]  ret  .Lfunc_end0:  .size _Z1fi, .Lfunc_end0-_Z1fi  .cfi_endproc                                          // -- 结束函数  .globl _Z2f2i // -- 开始函数 _Z2f2i  .p2对齐 2  .type _Z2f2i, @function  _Z2f2i: // @_Z2f2i  .cfi_startproc  // %bb.0:  adrp x8, .L__const._Z2f2i.array  添加 x8, x8, :lo12:.L__const._Z2f2i.array  添加 x8、x8、w0、sxtw #3  ldr x0, [x8, #8]  ret  .Lfunc_end1:  .size _Z2f2i, .Lfunc_end1-_Z2f2i  .cfi_endproc                                          // -- 结束函数  .type .L__const._Z2f2i.array, @object // @__const._Z2f2i.array  .section .rodata,"a", @progbits  .p2对齐 3  .L__const._Z2f2i.array:  .xword 1 // 0x1  .xword 2 // 0x2  .xword 3 // 0x3  .xword 4 // 0x4  .xword 5 // 0x5  .xword 6 // 0x6  .xword 7 // 0x7  .xword 8 // 0x8  .xword 999 // 0x3e7  .xword 10 // 0xa  .size .L__const._Z2f2i.array, 80    .ident "Ubuntu clang 版本 14.0.0-1ubuntu1"  .section ".note.GNU-stack","", @progbits  .addrsig  

但是,如果您稍微修改常量,则通常不会发生这种压缩(例如,如果您尝试将一个整数值附加到其中一个数组,代码将完全复制数组)。

为了评估代码例程的性能,我的第一道攻击总是计算指令。保持一切不变,如果您可以重写代码以生成更少的指令,它应该会更快。我也喜欢发现条件跳转,因为如果分支难以预测,那通常是你的代码可能受到影响的地方。

将一整套函数转换为汇编很容易,但随着项目变得越来越大,它变得不切实际。在 Linux 下,标准的“调试器”( gdb ) 是一个很好的工具,可以选择性地查看编译生成的汇编代码。让我们考虑一下我之前的博客文章, 在 Amazon Graviton 3 处理器上使用 SVE 快速过滤数字。在该博客文章中,我介绍了我在一个简短的 C++ 文件中实现的几个函数。为了检查结果,我只需将编译后的二进制文件加载到gdb中:

 $ gdb ./过滤器  

然后我可以检查函数……例如remove_negatives函数:

 (gdb) 设置打印 asm-demangle  (gdb) disas remove_negatives  函数 remove_negatives(int const*, long, int*) 的汇编代码转储:     0x00000000000022e4 <+0>: mov x4, #0x0 // #0     0x00000000000022e8 <+4>: mov x3, #0x0 // #0     0x00000000000022ec <+8>: cntw x6     0x00000000000022f0 <+12>: 同时 p0.s, xzr, x1     0x00000000000022f4 <+16>: 无     0x00000000000022f8 <+20>: ld1w {z0.s}, p0/z, [x0, x3, lsl #2]     0x00000000000022fc <+24>: cmpge p1.s, p0/z, z0.s, #0     0x0000000000002300 <+28>: 紧凑 z0.s, p1, z0.s     0x0000000000002304 <+32>: st1w {z0.s}, p0, [x2, x4, lsl #2]     0x0000000000002308 <+36>: cntp x5, p0, p1.s     0x000000000000230c <+40>: 添加 x3, x3, x6     0x0000000000002310 <+44>: 添加 x4, x4, x5     0x0000000000002314 <+48>: 同时 p0.s, x3, x1     0x0000000000002318 <+52>: b.ne 0x22f8 <remove_negatives(int const*, long, int*)+20> // b.any     0x000000000000231c <+56>: ret  汇编程序转储结束。  

在地址 52,我们有条件地回到地址 20。所以我们的主循环中总共有 9 条指令。根据我的基准测试(参见之前的博客文章),我使用每个 32 位字的 1.125 条指令,这与每个循环处理 8 个 32 位字是一致的。

另一种评估性能的方法是查看分支。让我们反汇编remove_negatives_scalar ,一个分支函数:

 (gdb) disas remove_negatives_scalar  函数 remove_negatives_scalar(int const*, long, int*) 的汇编代码转储:     0x0000000000002320 <+0>: cmp x1, #0x0     0x0000000000002324 <+4>: b.le 0x234c <remove_negatives_scalar(int const*, long, int*)+44>     0x0000000000002328 <+8>: 添加 x4, x0, x1, lsl #2     0x000000000000232c <+12>: mov x3, #0x0 // #0     0x0000000000002330 <+16>: ldr w1, [x0]     0x0000000000002334 <+20>: 添加 x0, x0, #0x4     0x0000000000002338 <+24>: tbnz w1, #31, 0x2344 <remove_negatives_scalar(int const*, long, int*)+36>     0x000000000000233c <+28>: str w1, [x2, x3, lsl #2]     0x0000000000002340 <+32>: 添加 x3, x3, #0x1     0x0000000000002344 <+36>: cmp x4, x0     0x0000000000002348 <+40>: b.ne 0x2330 <remove_negatives_scalar(int const*, long, int*)+16> // b.any     0x000000000000234c <+44>: ret  汇编程序转储结束。  

我们在地址 24 处看到分支(指令tbnz ),它有条件地跳过接下来的两条指令。我们有一个等效的“无分支”函数,称为remove_negatives_scalar_branchless 。让我们看看它是否确实是无分支的:

 (gdb) disas remove_negatives_scalar_branchless  函数 remove_negatives_scalar_branchless(int const*, long, int*) 的汇编代码转储:     0x0000000000002350 <+0>: cmp x1, #0x0     0x0000000000002354 <+4>: b.le 0x237c <remove_negatives_scalar_branchless(int const*, long, int*)+44>     0x0000000000002358 <+8>: 添加 x4, x0, x1, lsl #2     0x000000000000235c <+12>: mov x3, #0x0 // #0     0x0000000000002360 <+16>: ldr w1, [x0], #4     0x0000000000002364 <+20>: str w1, [x2, x3, lsl #2]     0x0000000000002368 <+24>: eor x1, x1, #0x80000000     0x000000000000236c <+28>: lsr w1, w1, #31     0x0000000000002370 <+32>: 添加 x3, x3, x1     0x0000000000002374 <+36>: cmp x0, x4     0x0000000000002378 <+40>: b.ne 0x2360 <remove_negatives_scalar_branchless(int const*, long, int*)+16> // b.any     0x000000000000237c <+44>: ret  汇编程序转储结束。  (gdb)  

除了循环产生的条件跳转(地址 40)之外,代码确实是无分支的。

在这个特殊的例子中,使用一个小的二进制文件,很容易找到我需要的函数。如果我加载一个包含许多编译函数的大型二进制文件怎么办?

让我检查一下来自 simdutf 库的基准二进制文件。它有很多功能,但让我们假设我正在寻找一个可以验证 UTF-8 输入的功能。我可以使用info 函数来查找与给定模式匹配的所有函数。

 (gdb) 信息函数 validate_utf8  匹配正则表达式“validate_utf8”的所有函数:    非调试符号:  0x0000000000012710 event_aggregate simdutf::benchmarks::BenchmarkBase::count_events<simdutf::benchmarks::Benchmark::run_validate_utf8(simdutf::implementation const&, unsigned long)::{lambda()#1}>(simdutf::benchmarks:: Benchmark::run_validate_utf8(simdutf::implementation const&, unsigned long)::{lambda()#1}, unsigned long) [clone .constprop.0]  0x0000000000012b54 simdutf::benchmarks::Benchmark::run_validate_utf8(simdutf::implementation const&, unsigned long)  0x0000000000018c90 simdutf::fallback::implementation::validate_utf8(char const*, unsigned long) const  0x000000000001b540 simdutf::arm64::implementation::validate_utf8(char const*, unsigned long) const  0x000000000001cd84 simdutf::validate_utf8(char const*, unsigned long)  0x000000000001d7c0 simdutf::internal::unsupported_implementation::validate_utf8(char const*, unsigned long) const  0x000000000001e090 simdutf::internal::detect_best_supported_implementation_on_first_use::validate_utf8(char const*, unsigned long) 常量  

您会看到info 函数给了我函数名称和函数地址。我对simdutf::arm64::implementation::validate_utf8感兴趣。此时,通过地址引用函数变得更容易:

 (gdb)disas 0x000000000001b540  函数 simdutf::arm64::implementation::validate_utf8(char const*, unsigned long) const 的汇编代码转储:     0x000000000001b540 <+0>: stp x29, x30, [sp, #-144]!     0x000000000001b544 <+4>: adrp x0, 0xa0000     0x000000000001b548 <+8>: cmp x2, #0x40     0x000000000001b54c <+12>: mov x29, sp     0x000000000001b550 <+16>: ldr x0, [x0, #3880]     0x000000000001b554 <+20>: mov x5, #0x40 // #64     0x000000000001b558 <+24>: movi v22.4s, #0x0     0x000000000001b55c <+28>: csel x5, x2, x5, cs // cs = hs, nlast     0x000000000001b560 <+32>: ldr x3, [x0]     0x000000000001b564 <+36>: str x3, [sp, #136]     0x000000000001b568 <+40>: mov x3, #0x0 // #0     0x000000000001b56c <+44>: 潜艇 x5, x5, #0x40     0x000000000001b570 <+48>: b.eq 0x1b7b8 <simdutf::arm64::implementation::validate_utf8(char const*, unsigned long) const+632> // b.none     0x000000000001b574 <+52>: adrp x0, 0x86000     0x000000000001b578 <+56>: adrp x4, 0x86000     0x000000000001b57c <+60>: 添加 x6, x0, #0x2f0     0x000000000001b580 <+64>: adrp x0, 0x86000  ...  

我已经缩短了输出,因为它太长了。当单个函数变大时,我发现将输出重定向到可以在其他地方处理的文件更方便。

 gdb -q ./benchmark -ex "设置分页关闭" -ex "设置打印 asm-demangle" -ex "disas 0x000000000001b540" -ex quit > gdbasm.txt  

有时我只是对做一些基本的统计感兴趣,比如弄清楚函数使用了哪些指令:

 $ gdb -q ./benchmark -ex "设置分页关闭" -ex "设置打印 asm-demangle" -ex "disas 0x000000000001b540" -ex 退出 | awk '{打印 $3}' |排序 |uniq -c |排序-r |头       32 和       24 汤匙       24分机       18厘米       17 或       16岁       16岁       14 路德       13 移动       10 电影  

我们看到这段代码中最常见的指令是and 。它让我确信代码已正确编译。我可以对所有生成的指令进行一些研究,考虑到我生成的代码,它们似乎都是足够的选择。

一般的教训是,查看生成的程序集并不难,只需很少的培训,它就可以让你成为一个更好的程序员。

原文: https://lemire.me/blog/2022/06/28/looking-at-assembly-code-with-gdb/

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme