汇编中的函数是什么?
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()? 例如。
我正在尝试学习汇编、编译器(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()? 例如。