【问题标题】:How to compile and dump assembly for a c library (string.h)?如何为 c 库 (string.h) 编译和转储程序集?
【发布时间】:2020-02-06 19:39:09
【问题描述】:

对于一个学校项目,我必须在汇编中进行大量的字符串操作。由于这样做很痛苦,我试图想出创新的方法来使用已经编程的字符串操作。我的想法是从 c 中的string.h 库中编译和转储程序集。然后我会将转储的程序集复制粘贴到我的程序中。在弄清楚每个函数的内存位置及其参数后,我想我基本上可以调用该函数了。

为了转储程序集,我首先编写了一个包含我想要的库的程序:

#include <stdio.h>
#include <string.h>

int main() {

  return 0;
}

然后我编译并转储程序集使用

gcc -o lib lib.c
objdump -d  *o

当我查看输出时,我注意到它不包含库的任何程序集。我的猜测是编译器优化不包括未使用的函数,或者当我使用 objdump 时库输出被隐藏:

lib:    file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
__text:
100000fa0:  55  pushq   %rbp
100000fa1:  48 89 e5    movq    %rsp, %rbp
100000fa4:  31 c0   xorl    %eax, %eax
100000fa6:  c7 45 fc 00 00 00 00    movl    $0, -4(%rbp)
100000fad:  5d  popq    %rbp
100000fae:  c3  retq

_main:
100000fa0:  55  pushq   %rbp
100000fa1:  48 89 e5    movq    %rsp, %rbp
100000fa4:  31 c0   xorl    %eax, %eax
100000fa6:  c7 45 fc 00 00 00 00    movl    $0, -4(%rbp)
100000fad:  5d  popq    %rbp
100000fae:  c3  retq

顺便说一句,我正在运行 OSX Catalina,但如果更容易的话,我可以切换到 Ubuntu 或其他操作系统。

如何转储 string.h 库的 asm?

【问题讨论】:

  • 仅包含 .h 文件不会向程序添加任何函数,除非您实际调用它们。同样调用库函数不会将程序集添加到您的程序中,它只是链接到共享库文件中的程序集。
  • 请注意,gcc 具有某些字符串函数的内置函数,因此启用优化后,您很可能会内联代码(如果您使用该函数)。如果您对库版本感兴趣,您当然可以反汇编库本身,而无需编译任何东西。
  • (跳出框框思考的要点。除非你真的应该为那个学校练习自己写它......)应该也可以转储标准库的内容。跨度>
  • 您可以直接查看源代码并尝试编译,而不是包含它:Linux string.c
  • @Frederik 该链接与标准 C 库无关。

标签: c gcc assembly objdump


【解决方案1】:

首先,让我先说这真的是XY problem

我的想法是从 c 中的 string.h 库中编译和转储程序集。然后我会将转储的程序集复制粘贴到我的程序中。

你不应该那样做。标准库有非常精心优化的函数,需要小心处理,并且非常非常复杂。换句话说,如果你正在学习汇编,它们对于教育目的基本上是无用的。

你真的应该用 C 编写你最喜欢的实现,然后编译它。


头文件(例如string.h)通常包含函数定义。它只包含他们的声明。真正的函数实际上已经编译成一个动态库对象,它安装在您的系统中(即库本身)。

当您编译程序时,编译器会自动将其链接到标准 C 库。根据this answer,在 OS X 中标准库应该位于/usr/lib/libSystem.B.dylib。在 Ubuntu 上,它通常是 /lib/x86_64-linux-gnu/libc.so.6。以下内容适用于两个平台,没有问题。

如果您想查看特定库函数的反汇编,您可以在库中运行objdump,将其通过管道连接到less,然后搜索函数名称:

$ objdump -d /usr/lib/libSystem.B.dylib | less

less 中,您可以通过键入/ 后跟函数名称进行搜索,然后按Enter 并使用n N 浏览匹配项。

或者,您可以将objdump 的输出转储到文件并使用文本编辑器进行检查:

 $ objdump -d /usr/lib/libSystem.B.dylib > libSystem.disasm

做这种事情时的问题是,标准库中的标准函数名称与您在string.h 中看到的名称相比有很多不同且更复杂的名称。在内部,使用的符号是不同的。例如,在 Linux 中使用printf 时,libc 中对应的符号实际上是__printf。例如,请参阅here

您可以通过编译使用它的程序并查看反汇编代码来找到标准库函数的真实符号名称,例如:

#include <string.h>
#include <stdio.h>

int main(void) {
    char s[100];

    scanf("%99s", s);
    size_t len = strlen(s);

    return 0;
}

然后运行:

$ gcc prog.c
$ objdump -d a.out
...
0000000000000720 <main>:
 720:   55                      push   %rbp
 721:   48 89 e5                mov    %rsp,%rbp
 724:   48 83 ec 70             sub    $0x70,%rsp
 728:   48 8d 45 90             lea    -0x70(%rbp),%rax
 72c:   48 89 c6                mov    %rax,%rsi
 72f:   48 8d 3d ae 00 00 00    lea    0xae(%rip),%rdi        # 7e4 <_IO_stdin_used+0x4>
 736:   b8 00 00 00 00          mov    $0x0,%eax
 73b:   e8 90 fe ff ff          callq  5d0 <__isoc99_scanf@plt>
 740:   48 8d 45 90             lea    -0x70(%rbp),%rax
 744:   48 89 c7                mov    %rax,%rdi
 747:   e8 74 fe ff ff          callq  5c0 <strlen@plt>
 74c:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 750:   b8 00 00 00 00          mov    $0x0,%eax
 755:   c9                      leaveq
 756:   c3                      retq
 757:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
 75e:   00 00

你可以看到在我的例子中scanf实际上是__isoc99_scanf,而strlen没有改变。

然后我可以查找strlen 的反汇编,在我的系统(Ubuntu)上如下:

$ objdump -d /lib/x86_64-linux-gnu/libc.so.6 | less
...
0000000000080650 <strlen@@GLIBC_2.2.5>:
   80650:       66 0f ef c0             pxor   %xmm0,%xmm0
   80654:       66 0f ef c9             pxor   %xmm1,%xmm1
   80658:       66 0f ef d2             pxor   %xmm2,%xmm2
   8065c:       66 0f ef db             pxor   %xmm3,%xmm3
   80660:       48 89 f8                mov    %rdi,%rax
   80663:       48 89 f9                mov    %rdi,%rcx
   80666:       48 81 e1 ff 0f 00 00    and    $0xfff,%rcx
   8066d:       48 81 f9 cf 0f 00 00    cmp    $0xfcf,%rcx
   80674:       77 6a                   ja     806e0 <strlen@@GLIBC_2.2.5+0x90>
   80676:       f3 0f 6f 20             movdqu (%rax),%xmm4
   8067a:       66 0f 74 e0             pcmpeqb %xmm0,%xmm4
   8067e:       66 0f d7 d4             pmovmskb %xmm4,%edx
   80682:       85 d2                   test   %edx,%edx
   80684:       74 04                   je     8068a <strlen@@GLIBC_2.2.5+0x3a>
   80686:       0f bc c2                bsf    %edx,%eax
   80689:       c3                      retq
   8068a:       48 83 e0 f0             and    $0xfffffffffffffff0,%rax
   8068e:       66 0f 74 48 10          pcmpeqb 0x10(%rax),%xmm1
   80693:       66 0f 74 50 20          pcmpeqb 0x20(%rax),%xmm2
   80698:       66 0f 74 58 30          pcmpeqb 0x30(%rax),%xmm3
   8069d:       66 0f d7 d1             pmovmskb %xmm1,%edx
   806a1:       66 44 0f d7 c2          pmovmskb %xmm2,%r8d
   806a6:       66 0f d7 cb             pmovmskb %xmm3,%ecx
   806aa:       48 c1 e2 10             shl    $0x10,%rdx
   ...
   ...

如您所见,由于 glibc 的作者多年来应用了大量的优化和手动调整,即使是这样一个简单的函数实际上也是一个看似无法理解的复杂指令丛林。

【讨论】:

  • “无法理解复杂指令的丛林”是夸大其词。例如,许多人以逆向工程恶意软件为生,这比 strlen 更复杂,因为您事先知道它的用途。当然,我完全同意尝试对循环展开的代码进行逆向工程以节省学校作业的时间是一个非常糟糕的主意。
  • @Gene: 或者如果你想了解 glibc 函数,请阅读 注释 手写 asm 源代码!它使用一些宏,但在其他方面看起来像普通的 AT&T 语法。例如SSE2 版本code.woboq.org/userspace/glibc/sysdeps/x86_64/strlen.S.html(另请参阅Why does glibc's strlen need to be so complicated to run quickly?,了解有关 C 回退与手写 asm 版本的一些信息,带有链接。)glibc 的 memcpy / memset 也很好,using 2 overlapping stores for small buffers
  • @Gene 我知道,我自己做一些 RE 是为了好玩,我只是从新手的角度讲,因为 OP 正在学习汇编。我可能夸大了那里的夸张:') 虽然恶意软件通常不会使用所有那些花哨的优化操作码,但它比荒谬的指令更具自我修改和代码混淆。
【解决方案2】:

您提出的建议是一个坏主意,因为生产库代码是在编译时优化的。优化后的代码并非无法理解,但可能会很复杂。为什么?例如,gcc 经常会选择您可能不想或不需要了解的向量指令。它将简单的循环展开为长长的重复代码。它将以不直观的顺序重新排列指令,以保持处理器流水线满载。在您学习时,这些都是混淆的来源。

可以高效地学习的是通过轻度优化编译 C。

Godbot Compiler Explorer 很适合这个。给它一些代码片段,看看不同的编译器对不同的优化级别做了什么。上面的链接显示strlenHere'sstrcpyHere's one that's not in the standard library at all。它将指向 char 的指针前进到字符串的末尾或分隔符的第一次出现。即,它是一个简单的字符串解析器。

【讨论】:

    【解决方案3】:

    通用方法:

    1. 查找软件包,适用于类 Debian 的 Linux:
    $ apt-file search string.h
    

    当然,这是 glibc。

    1. 获取源例如glibc 2.31 并编译它:
    $ ./configure --prefix=/usr --enable-kernel=4.0.0 --disable-profile --with-gnu-ld --enable-stack-protector=strong
    $ make
    
    1. 函数的实现通常具有相同的名称,因此请查找来源:
    $ find . -type f -name "strstr.c"
    

    这是字符串/strstr.c

    1. 反汇编默认编译版本:
    $ objdump -d string/strstr.o > strstr1.asm
    
    1. 制作具有自定义优化的新版本: 删除目标文件:
    $ rm string/strstr.o`
    

    使用命令输出重新制作:

    $ make V=1 &>strstrr.txt
    
    • 这是用于“bash”,否则使用“脚本”命令

    从 strstrr.txt 中获取 gcc 命令,根据需要进行修改(优化、处理器类型...),例如将 -O2 更改为 O10,然后运行:

    $ cd strings
    $ gcc ../sysdeps/x86_64/multiarch/strstr.c -c -std=gnu11 -fgnu89-inline  -g -O10 -Wall -Wwrite-strings -Wundef -Werror -fmerge-all-constants -frounding-math -fstack-protector-strong -Wstrict-prototypes -Wold-style-definition -fmath-errno      -ftls-model=initial-exec      -I../include -I/home/yury/LFSC/cross1/src/bglibcn/string  -I/home/yury/LFSC/cross1/src/bglibcn  -I../sysdeps/unix/sysv/linux/x86_64/64  -I../sysdeps/unix/sysv/linux/x86_64  -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86  -I../sysdeps/x86/nptl  -I../sysdeps/unix/sysv/linux/wordsize-64  -I../sysdeps/x86_64/nptl  -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux  -I../sysdeps/nptl  -I../sysdeps/pthread  -I../sysdeps/gnu  -I../sysdeps/unix/inet  -I../sysdeps/unix/sysv  -I../sysdeps/unix/x86_64  -I../sysdeps/unix  -I../sysdeps/posix  -I../sysdeps/x86_64/64  -I../sysdeps/x86_64/fpu/multiarch  -I../sysdeps/x86_64/fpu  -I../sysdeps/x86/fpu/include -I../sysdeps/x86/fpu  -I../sysdeps/x86_64/multiarch  -I../sysdeps/x86_64  -I../sysdeps/x86  -I../sysdeps/ieee754/float128  -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96  -I../sysdeps/ieee754/dbl-64/wordsize-64  -I../sysdeps/ieee754/dbl-64  -I../sysdeps/ieee754/flt-32  -I../sysdeps/wordsize-64  -I../sysdeps/ieee754  -I../sysdeps/generic  -I.. -I../libio -I.   -D_LIBC_REENTRANT -include /home/yury/LFSC/cross1/src/bglibcn/libc-modules.h -DMODULE_NAME=libc -include ../include/libc-symbols.h       -DTOP_NAMESPACE=glibc -o /home/yury/LFSC/cross1/src/bglibcn/string/strstr.o -MD -MP -MF /home/yury/LFSC/cross1/src/bglibcn/string/strstr.o.dt -MT /home/yury/LFSC/cross1/src/bglibcn/string/strstr.o
    $ objdump -d string/strstr.o > strstr2.asm
    

    代码会有所不同:

    $ diff strstr1.asm strstr2.asm
    

    因此,您可以将所需的代码复制粘贴到汇编程序中以节省时间。

    【讨论】:

    • 大部分 glibc 字符串函数都有 x86 asm 实现。编译通用后备可以为您提供一些过于复杂的版本的 asm,该版本可能一次执行 4 个字节,并使用 bithack 来检查可能的字符串结尾。或者,如果您找到code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/…,那么您正在查看动态链接器运行时 CPU 调度代码,该代码根据主机选择要解析的实际实现。如果你想从 glibc 复制 asm,你还不如直接抢 x86_64/multiarch/strstr-sse2-unaligned.S source
    • glibc 是多架构库(没有手机):``` $ find . -type f -name "strstr.c" ./sysdeps/x86_64/multiarch/strstr.c ./sysdeps/powerpc/powerpc64/multiarch/strstr.c ./sysdeps/s390/strstr.c ./string/strstr.c ``` 标准 C 库的另一个多架构实现是 en.wikipedia.org/wiki/Bionic_(software) Platform‎:‎x86‎、‎x86-64‎、‎ARM‎、‎ARM64‎、‎MIPS‎、‎MI...
    • glibc “multiarch” 并不意味着“可移植”,它意味着它处理 x86-64 CPU 的一些变体或 PowerPC 的一些变体的运行时调度,其中最佳的实现选择是在动态链接时选择。 Glibc 的 portable 后备 C 实现是 code.woboq.org/userspace/glibc/string/strstr.c.html。 (这个文件是 #include&lt;&gt; 由 x86 multiarch .c 文件编写的,作为没有高效 SSE2 未对齐加载/存储的 CPU 的后备。strchr 等其他函数没有 C 后备,始终是 asm 版本。)跨度>
    猜你喜欢
    • 2011-11-25
    • 1970-01-01
    • 2011-12-22
    • 1970-01-01
    • 2018-05-10
    • 1970-01-01
    • 2011-09-08
    • 1970-01-01
    相关资源
    最近更新 更多