8

我有兴趣了解更多 x86/x86_64 程序集。唉,我在Mac上。没问题,对吧?

$ gcc --version
i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 
5658) (LLVM build 2336.11.00)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO 
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

我用 C 语言编写了一个简单的“Hello World”,以了解我必须编写什么样的代码。我在大学时做了一点 x86,并查阅了许多教程,但没有一个看起来像我在这里看到的怪异输出:

.section    __TEXT,__text,regular,pure_instructions
.globl  _main
.align  4, 0x90
_main:
Leh_func_begin1:
pushq   %rbp
Ltmp0:
movq    %rsp, %rbp
Ltmp1:
subq    $32, %rsp
Ltmp2:
movl    %edi, %eax
movl    %eax, -4(%rbp)
movq    %rsi, -16(%rbp)
leaq    L_.str(%rip), %rax
movq    %rax, %rdi
callq   _puts
movl    $0, -24(%rbp)
movl    -24(%rbp), %eax
movl    %eax, -20(%rbp)
movl    -20(%rbp), %eax
addq    $32, %rsp
popq    %rbp
ret
Leh_func_end1:

.section    __TEXT,__cstring,cstring_literals
L_.str:
.asciz   "Hello, World!"

.section    __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame0:
Lsection_eh_frame:
Leh_frame_common:
Lset0 = Leh_frame_common_end-Leh_frame_common_begin
.long   Lset0
Leh_frame_common_begin:
.long   0
.byte   1
.asciz   "zR"
.byte   1
.byte   120
.byte   16
.byte   1
.byte   16
.byte   12
.byte   7
.byte   8
.byte   144
.byte   1
.align  3
Leh_frame_common_end:
.globl  _main.eh
_main.eh:
Lset1 = Leh_frame_end1-Leh_frame_begin1
.long   Lset1
Leh_frame_begin1:
Lset2 = Leh_frame_begin1-Leh_frame_common
.long   Lset2
Ltmp3:
.quad   Leh_func_begin1-Ltmp3
Lset3 = Leh_func_end1-Leh_func_begin1
.quad   Lset3
.byte   0
.byte   4
Lset4 = Ltmp0-Leh_func_begin1
.long   Lset4
.byte   14
.byte   16
.byte   134
.byte   2
.byte   4
Lset5 = Ltmp1-Ltmp0
.long   Lset5
.byte   13
.byte   6
.align  3
Leh_frame_end1:


.subsections_via_symbols

现在......也许事情发生了一些变化,但这并不完全友好,即使对于汇编代码也是如此。我很难解决这个问题......有人可以帮助分解这段代码中发生的事情以及为什么需要它吗?

非常非常感谢提前。

4

3 回答 3

11

由于问题实际上是关于那些奇怪的标签和数据,而不是关于代码本身,所以我只会对它们进行一些说明。

如果程序的一条指令导致执行错误(例如除以 0 或访问不可访问的内存区域或尝试执行特权指令),则会导致异常(不是 C++ 类型的异常,而是中断类型它)并强制 CPU 在 OS 内核中执行适当的异常处理程序。如果我们完全禁止这些异常,那么故事就会很短,操作系统会简单地终止程序。

但是,让程序处理自己的异常是有好处的,因此操作系统处理程序中的主要异常处理程序会将一些异常反映回程序中进行处理。例如,程序可以尝试从异常中恢复,或者可以在终止之前保存有意义的崩溃报告。

在任何一种情况下,了解以下内容都会很有用:

  • 发生异常的函数,而不仅仅是其中的违规指令
  • 调用那个函数的函数,调用那个函数的函数等等

并且可能(主要用于调试):

  • 生成此指令的源代码文件的行
  • 进行这些函数调用的行
  • 函数参数

为什么我们需要知道调用树?

好吧,如果程序注册了自己的异常处理程序,它通常会做一些类似 C++trycatch块的事情:

fxn()
{
  try
  {
    // do something potentially harmful
  }
  catch()
  {
    // catch and handle attempts to do something harmful
  }
  catch()
  {
    // catch and handle attempts to do something harmful
  }
}

如果这两个都没有catches捕获,则异常传播到 的调用者,fxn并可能传播到 的调用者的调用者fxn,直到有一个catch捕获异常或直到简单地终止程序的默认异常处理程序。

因此,您需要知道每个覆盖的代码区域,并且如果立即/没有捕获异常并且它必须冒泡try,您需要知道如何到达下一个最近try的区域(例如,在 的调用者中)。fxntrycatch

块的范围try和位置catch很容易在可执行文件的一个特殊部分中编码,并且它们很容易使用(只需对这些范围中的违规指令地址进行二进制搜索)。但是找出下一个外部try块更难,因为您可能需要从函数中找出发生异常的返回地址。

而且您可能并不总是依赖于rbp+8指向堆栈上的返回地址,因为编译器可能会以rbp不再涉及访问函数参数和局部变量的方式优化代码。您也可以通过它们访问它们rsp+something并保存一个寄存器和一些指令,但考虑到不同的函数在堆栈上为本地变量分配不同数量的字节以及传递给其他函数的参数并进行不同的调整,rsp因此只有rsp'不足以找出返回地址和调用函数。rsp可以与返回地址在堆栈上的位置相距任意数量的字节。

对于这种情况,编译器在可执行文件的专用部分中包含有关函数及其堆栈使用的附加信息。异常处理代码检查此信息并在异常必须传播到调用函数及其try/catch块时正确展开堆栈。

因此,以下数据_main.eh包含该附加信息。请注意,它main()通过引用Leh_func_begin1和显式编码 的开头和大小Leh_func_end1-Leh_func_begin1。这条信息允许异常处理代码将main()'s指令标识为main()'s.

它似乎main()也不是很独特,它的一些堆栈/异常信息与其他函数相同,在它们之间共享它是有意义的。所以有一个参考Leh_frame_common

我无法进一步评论_main.eh这些常量的结构和确切含义,144因为13我不知道这些数据的格式。但通常不需要知道这些细节,除非他们是编译器或调试器开发人员。

我希望这能让您了解这些标签和常量的用途。

于 2013-03-09T06:09:19.017 回答
4

好的,让我们试一试

// 第一段代码,声明必须在 32 位边界上对齐的主函数。

更新:我对 .align 指令的解释可能是错误的。请参阅下面的气体文档。

.section    __TEXT,__text,regular,pure_instructions
.globl  _main
.align  4, 0x90
_main:

存储前一个基指针并为局部变量分配堆栈空间。

Leh_func_begin1:
pushq   %rbp
Ltmp0:
movq    %rsp, %rbp
Ltmp1:
subq    $32, %rsp
Ltmp2:

将参数压入堆栈并调用 puts()

movl    %edi, %eax
movl    %eax, -4(%rbp)
movq    %rsi, -16(%rbp)
leaq    L_.str(%rip), %rax
movq    %rax, %rdi
callq   _puts

将返回值放入堆栈,释放本地内存,恢复基指针并返回。

movl    $0, -24(%rbp)
movl    -24(%rbp), %eax
movl    %eax, -20(%rbp)
movl    -20(%rbp), %eax
addq    $32, %rsp
popq    %rbp
ret
Leh_func_end1:

下一部分,也是代码部分,包含要打印的字符串。

.section    __TEXT,__cstring,cstring_literals
L_.str:
.asciz   "Hello, World!"

其余的我不知道,可能是用作 c 启动代码和/或调试信息的数据。

.section    __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
...

更新:关于 .align 指令的文档来自:http: //sourceware.org/binutils/docs-2.23.1/as/Align.html#Align

“指定所需对齐的方式因系统而异。对于使用 ELF 的 arc、hppa、i386、i860、iq2000、m68k、or32、s390、sparc、tic4x、tic80 和 xtensa,第一个表达式是对齐请求字节。例如 `.align 8' 使位置计数器前进,直到它是 8 的倍数。如果位置计数器已经是 8 的倍数,则无需更改。对于 tic54x,第一个表达式是对齐请求字.

对于其他系统,包括使用 a.out 格式的 ppc、i386、arm 和 strongarm,它是位置计数器在前进后必须具有的低位零位的数量。例如,`.align 3' 将位置计数器推进到 8 的倍数。如果位置计数器已经是 8 的倍数,则无需更改。

这种不一致是由于 GAS 必须模拟的这些系统的各种本地汇编程序的不同行为。GAS 还提供了 .balign 和 .p2align 指令,稍后描述,它们在所有架构中具有一致的行为(但特定于 GAS)。”

//jk

于 2013-03-08T12:16:57.073 回答
2

您可以在此处此处找到与指令相关的几乎所有问题的答案。

例如:

.section    __TEXT,__text,regular,pure_instructions

声明一个以__TEXT,__text默认节类型命名的节,并指定该节将仅包含机器代码(即不包含数据)。


.globl _main
使_main标签(符号)全局化,以便链接器可以看到它。


.align 4, 0x90
将位置计数器与下一个 2^4 (==16) 字节边界对齐。中间的空间将填充值 0x90 (==NOP)。

至于代码本身,它显然做了很多冗余的中间加载和存储。尝试按照评论员的建议启用优化进行编译,您应该会发现生成的代码会更有意义。

于 2013-03-08T12:12:40.113 回答