【问题标题】:What is function in assembly?汇编中的函数是什么?
【发布时间】:2020-06-29 08:42:22
【问题描述】:

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

我可以只用 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 检查该二进制文件。然后我可以找到“功能”。

附言

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

在这里我可以看到什么是函数。

什么是函数不同的NOTYPE标签?

【问题讨论】:

  • 对,汇编语言并没有真正的功能,只是实现该概念的工具(例如,在某处跳转和存储返回地址 = 调用,间接跳转到返回地址 = ret)。执行模型是纯粹的顺序和本地的,一次一条指令(在大多数 ISA 上,但有些 ISA 是 VLIW,例如一次执行 3 条,但在范围内仍然是本地的),每条指令都只是一个定义明确的指令更改为架构状态。你问的是这个吗?
  • 您即将完成该课程。他们会教你。期望在一个 SO 问题中回答如此广泛的问题是非常荒谬的。
  • 是的,C 编译器不会在函数之外放置任何指令。 (从技术上讲,间接调用main_start 入口点不是一个函数;它不能返回并且必须进行exit 系统调用,但这是用asm 编写的并且是libc 的一部分。它不是生成的由 C 编译器正确。)
  • 我可以在您的帖子中看到至少三个问题,并且没有一个问题有简单的是/否答案。
  • ELF 符号元数据可以由一些汇编器设置,例如在 NASM 中,global main:function 将符号类型标记为 FUNC。 (nasm.us/doc/nasmdoc8.html#section-8.9.5)。 GAS 语法等价物(C 编译器发出)是.type main, function。例如在godbolt.org 上添加一些代码并禁用过滤以在编译器输出中查看 asm 指令。但请注意,这只是链接器和调试器使用的元数据; CPU在执行时看不到这一点。这就是为什么没有人为 NASM 示例而烦恼的原因。

标签: assembly compiler-construction llvm-ir


【解决方案1】:

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 示例而烦恼的原因。


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

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

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

从技术上讲,间接调用main_start 入口点不是函数;它无法返回,必须进行exit 系统调用,但这是用 asm 编写的,是 libc 的一部分。它不是由 C 编译器正确生成的,仅与 C 编译器的输出链接以生成工作程序。)例如,请参见 Linux x86 Program Start Up or - How the heck do we get to main()?

【讨论】:

    【解决方案2】:

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

    函数名称基本上是表示地址的标签。在高级语言之外没有函数、变量类型(无符号整数、浮点数、布尔值等)、地址、数据和指令的真正概念。装配通常没有这些概念的真正概念,因为它们不存在于那个级别。当计算结构中的偏移量以访问某些项目时,基地址和偏移量只是在加法发生时的数字,它们既不是地址也不是偏移量,它们只是该指令执行的短暂时刻的地址,一个时钟当地址被锁存并通过逻辑发送到总线时循环,否则它只是位。

    现在说一些汇编语言的声明使用了诸如 FUNCTION 或 PROCEDURE 之类的词,但它们不一定像您有明确划分界限的高级语言。

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

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

    对于特定的编译器/命令行会产生这个(编译和组装输出的反汇编)

    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
    

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

    那么从这个角度来看,你对函数边界的定义是什么?它在返回时结束吗?如果是这样,那么在函数的返回之外还有函数的项目。局部全局(静态局部)显然位于函数之外的 .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
    

    如果您查看基本上针对 gnu 汇编程序的 clangs 输出,那么这个我们的 gnu 汇编程序汇编语言您会看到一个函数的概念,可能用于调试器目的,它对处理器没有任何意义,没有那里的概念,也不是真正的汇编器。

        .type   fun0,%function
    

    因为这是 arm,这可能作为高级概念的函数定义,但对于 arm/thumb-interwork 而言,链接器生成正确的地址至关重要,它基本上告诉汇编器告诉链接器这个标签是一个函数标签,这意味着在这种情况下,拇指函数标签地址是地址 ORRed 为 1,手臂函数标签地址 ORRed 为 0 或未修改。

    他们在这里双重浸入,因为

        .thumb_func
    fun0:
    

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

    如果你删除

    .fnstart
    .fnend
    

    没有什么不好的事情发生

    对于拇指,您也可以删除 .type 函数,除了使用与高级语言相关的工具(调试器等)的人之外,没有人注意到生成的代码很好并且工作正常。 (arm 模式没有 .arm_func 等效项,您必须使用 .type 函数才能使链接器正常工作)

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-01
      • 2015-10-03
      • 2012-09-17
      相关资源
      最近更新 更多