汇编中的函数是什么?

What is function in assembly?

我正在尝试学习汇编、编译器(LLVM) 和lifter。

我可以通过 nasm 编写汇编代码。(如 this

下面是我的汇编代码。

section .data
hello_string db "Hello World!", 0x0d, 0x0a
hello_string_len equ $ - hello_string

section .text
global _start

_start:
    mov eax, 4 ; eax <- 4, syscall number (print) But, never execute.
    mov ebx, 1 ; ebx <- 1, syscall argument1 (stdout) But, never execute.
    mov ecx, hello_string ; ecx <- exit_string, syscall argument2 (string ptr) But, never execute.
    mov edx, hello_string_len ; edx <- exit_string_len, syscall argument3 (string len) But, never execute.
    int 0x80; ; syscall But, never execute.
    mov eax, 1 ; eax <- 1, syscall number (exit) But, never execute.
    mov ebx, 0 ; ebx <- 0, syscall argument1 (return value) But, never execute.
    int 0x80; syscall But, never execute.

;nasm -felf32 hello.x86.s -o hello.o
;ld -m elf_i386 hello.o -o hello.out

然后我检查二进制文件。

在这里,我找不到函数。我同意 call 和 ret 指令是一些指令的组合。

$readelf -s hello.o
Symbol table '.symtab' contains 7 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.x86.s
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    2 
     4: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 hello_string
     5: 0000000e     0 NOTYPE  LOCAL  DEFAULT  ABS hello_string_len
     6: 00000000     0 NOTYPE  GLOBAL DEFAULT    2 _start

但是。如果我编译 c 程序并通过 readelf 检查该二进制文件。然后我可以找到“功能”。

P.S

$readelf -s function.o | grep FUNC
     3: 0000000000000000    18 FUNC    GLOBAL DEFAULT    2 add
     4: 0000000000000020    43 FUNC    GLOBAL DEFAULT    2 main

在这里我可以看到什么是功能。

什么是功能不同的 NOTYPE 标签?

首先,汇编语言特定于 assembler,即读取它的工具。不是目标(arm、x86、mips 等)。

函数名称基本上是表示地址的标签。在高级语言之外没有函数、变量类型(unsigned int、float、boolean 等)、地址与数据与指令的真正概念。汇编通常对这些概念没有真正的概念,因为它们不存在于那个级别。当计算结构中的偏移量以访问某些项目时,基地址和偏移量只是数字,当发生添加时它​​们不是地址也不是偏移量,它们只是该指令执行的短暂时刻的地址,一个时钟当地址被锁存并通过逻辑向总线发送时的循环,否则它只是位。

现在说一些汇编语言有使用像 FUNCTION 或 PROCEDURE 这样的词的声明,但这些不一定像你有明确划分边界的高级语言。

然后是编译器生成的代码与手工生成的代码,手工生成的代码没有这些边界概念。

unsigned int fun0 ( void )
{
    return(0x12345678);
}
void fun1 ( unsigned int y )
{
    static unsigned int x=5;
    x=x+y;
}

对于特定的 compiler/command 行产生这个(编译和组装输出的反汇编)

Disassembly of section .text:

00000000 <fun0>:
   0:   4800        ldr r0, [pc, #0]    ; (4 <fun0+0x4>)
   2:   4770        bx  lr
   4:   12345678

00000008 <fun1>:
   8:   4902        ldr r1, [pc, #8]    ; (14 <fun1+0xc>)
   a:   680a        ldr r2, [r1, #0]
   c:   1810        adds    r0, r2, r0
   e:   6008        str r0, [r1, #0]
  10:   4770        bx  lr
  12:   46c0        nop         ; (mov r8, r8)
  14:   00000000 

Disassembly of section .data:

00000000 <fun1.x>:
   0:   00000005

函数名称只是标签,这意味着它们只是地址,从处理器的角度来看,没有标签的概念,更不用说函数了。

那么从这个角度来看,您对函数边界的定义是什么?它是否在 return 处结束?如果是这样,那么函数的 return 之外还有函数项。局部全局(静态局部)显然位于函数之外的 .data 部分。

    .globl  fun0
    .p2align    2
    .type   fun0,%function
    .code   16
    .thumb_func
fun0:
    .fnstart
    ldr r0, .LCPI0_0
    bx  lr
    .p2align    2
.LCPI0_0:
    .long   305419896
.Lfunc_end0:
    .size   fun0, .Lfunc_end0-fun0
    .cantunwind
    .fnend

如果你看一下 clangs 输出,它基本上是针对 gnu assembler 的,所以这个我们的 gnu assembler 汇编语言你会看到一个函数的概念,可能用于调试器目的, none of it means anything to the processor,那里没有概念,也没有真正的assembler。

    .type   fun0,%function

因为这是 arm,这可能作为高级概念的函数定义,但对于 arm/thumb-interwork 来说,它对于 linker 生成正确的地址至关重要,它基本上告诉 assembler 告诉 linker 这个标签是一个函数标签,这意味着在这种情况下,thumb 函数标签地址是地址与 1 或,而 arm 函数标签地址是与零或未修改。

因为

他们在这里加了两次
    .thumb_func
fun0:

还用一件事来处理 ORRed。 type , 函数可能会添加调试器信息,当用户认为他们在高级代码上使用调试器时,他们希望在其中看到调试函数的错觉。

如果你删除

.fnstart
.fnend

没有什么不好的事情发生

根据经验,您也可以删除 .type 函数,除了可能使用与高级语言相关的工具(调试器等)的人之外,没有人注意到生成的代码很好并且工作正常。 (手臂模式没有 .arm_func 等效项,您必须使用 .type 函数才能让 linker 正常工作)

在 arm 之外,也许还有 mips(也有 32/16 位指令集混合)我不知道您在生成工作代码时是否需要关心这些类型的指令。

这里再次汇编是特定于assembler的,一个生成汇编的编译器(gnu等,使用工具链是理智的模型),需要为特定的assembler 显然,并且受那里的功能约束。用户已经产生了期望,例如通过高级语言单步执行的错觉和高级语言中的其他调试,而不是现实,并且工具已经发展以在工具链中提供更多调试信息(编译,assemble,link) 以便最终的二进制文件取决于构建选项可以具有该调试信息(并且在需要的地方可以不优化代码以便调试视图工作)。

其他问题,在线汇编是编译器特定的功能,不一定是高级语言标准的一部分。而且它不是真正的汇编,或者说它是一种新的汇编语言,因为编译器是工具,所以它 can/does 不同于工具链中 assembler 的汇编语言。但是许多编译器根据语言支持某种形式的内联汇编(没有理由期望它在编译器之间兼容),因此在这种情况下,您可以将指令放入 C 代码中。这是一种绝望的行为,但在技术上是可行的。

LLVM ir 或字节码是它自己的指令集和语言,完全独立于目标或高级,它完全是另一头野兽。 Sane 编译器设计有某种形式的内部 structures/code 来跟踪编译后的代码向目标输出(通常是汇编语言或机器代码)的过程,它完全是另一种野兽。

我对 llvm 的理解是您使用编译器 (clang) 作为您的“assembler”,这令人不安,但他们就是这样做的。在那个视图中,我不认为它是内联汇编而是真正的汇编。默认情况下,linker 不是根据我的经验构建的,因此使用了 gnus linker。至少对于裸机工作,对象在 llvm 和 gnu binutils 之间兼容,clang 或 llc 的汇编输出与 binutils 汇编语言(gnu assembler)等兼容。而 gnu disassembler 在使用 compiler/assembler 输出进行调试方面优于 llvms。 llvm 在内部做事方面取得了长足的进步,不需要 binutils,如果你在 linker 中构建,那么你不需要 binutils(对于你单独执行这些步骤的项目,而不仅仅是 clang hello.c -o 你好).

ELF 符号元数据可以由一些汇编程序设置,例如在 NASM、global main:function 中将符号类型标记为 FUNC。 (https://nasm.us/doc/nasmdoc8.html#section-8.9.5).

等效的 GAS 语法(C 编译器发出的语法)是 .type main, function。例如在 https://godbolt.org 上放置一些代码并禁用过滤以在编译器输出中查看 asm 指令。

但请注意,这只是供链接器和调试器使用的元数据; CPU 在执行时看不到。这就是为什么没有人为 NASM 个示例而烦恼。


汇编语言并没有真正的功能,只有实现该概念的工具,例如跳转并在某处存储一个 return 地址 = call,间接跳转到一个 return 地址 = ret。在 x86 上,return 个地址在堆栈上被压入和弹出。

执行模型是纯顺序和本地的,一次一条指令(在大多数 ISA 上,但有些 ISA 是 VLIW,例如一次执行 3 条,但在范围内仍然是本地的),每条指令仅对架构状态进行明确定义的更改。 CPU 本身不知道或不关心它是“在一个函数中”或任何关于嵌套的事情,除了 return-地址预测器堆栈,它乐观地假设 ret 实际上会使用一个return 地址由相应的 call 推送。但这是性能优化;如果代码做了一些奇怪的事情(例如上下文切换),你有时会得到不匹配 call/ret。

C 编译器不会在函数之外放置任何指令。

从技术上讲,间接调用 main_start 入口点不是函数;它不能 return 并且必须进行 exit 系统调用,但这是用 asm 编写的并且是 libc 的一部分。它不是由 C 编译器本身生成的,仅与 C 编译器的输出链接以生成工作程序。)参见 Linux x86 Program Start Up 或 - 我们到底如何到达 main()? 例如。