我阅读了我们编写源代码(高级语言)的所有地方,编译器将其转换为机器代码(低级语言)。然后我读到有一个汇编程序,它将汇编代码转换为机器代码。然后在区分编译器和解释器时,我读到编译器首先将整个代码转换为目标代码,而解释器通过跳过目标代码直接转换为机器代码。现在我很困惑,我想到了以下问题:
- 汇编代码从哪里出来,如果编译器直接将源代码转换为机器代码?
- 目标代码和机器代码有什么区别?
- 谁将源代码转换为汇编代码?
- 什么是高级语言和低级语言,如何区分它们?
- 汇编代码和目标代码是高级还是低级?
我阅读了我们编写源代码(高级语言)的所有地方,编译器将其转换为机器代码(低级语言)。然后我读到有一个汇编程序,它将汇编代码转换为机器代码。然后在区分编译器和解释器时,我读到编译器首先将整个代码转换为目标代码,而解释器通过跳过目标代码直接转换为机器代码。现在我很困惑,我想到了以下问题:
您的大多数问题都没有简单的答案,因为它可能因编译器而异。一些编译器会生成其他高级语言,例如 C。
通常,对于使用汇编器的编译器,后端将发出一个临时 asm 文件,汇编器将其转换为目标代码。如果您有权访问 GCC,您可以看到它与该-v
选项一起使用的命令链。例如,对于 C 源
int main(){ return 1; }
命令
gcc -v -o 测试 test.c
输出(我过滤了很多)
cc1 test.c -o /tmp/cc9Otd7R.s
as -v --64 -o /tmp/cc5KhWEM.o /tmp/cc9Otd7R.s
collect2 --eh-frame-hdr -m elf_x86_64 -o test /tmp/cc5KhWEM.o
48 83 ec 10 子 rsp,0x10
前四个字是机器码的 4 个字节,后面是汇编程序。
根据第 1 点,这将是编译器后端。
5.这有点主观,但组装水平很低。您通常不会手动修改目标代码(我有时会使用十六进制编辑器这样做,但此类更改通常非常小)
汇编程序采用汇编语言、更易于人类读写的处理器指令,并将其转换为机器代码或这些指令的二进制版本。
汇编语言向量.s
.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.word foo
.word foo
.word foo
.word foo
.word foo
.word foo
.thumb_func
reset:
bl fun
.thumb_func
foo:
b foo
.globl dummy
dummy:
bx lr
组装然后拆卸
arm-none-eabi-as vectors.s -o vectors.o
arm-none-eabi-objdump -D vectors.o > vectors.list
拆解相关部分
Disassembly of section .text:
00000000 <_start>:
0: 20001000
...
00000020 <reset>:
20: f7ff fffe bl 0 <fun>
00000024 <foo>:
24: e7fe b.n 24 <foo>
00000026 <dummy>:
26: 4770 bx lr
.words 不是将数据放入二进制/输出中的指令。在这种情况下,我正在生成一个向量表。反汇编程序尚未显示所有内容,我们将看到其余部分。汇编器留下了占位符,我们很快就会看到供链接器填充。所以这就是一个对象看起来像汇编已经变成机器代码的样子。组装 bx lr,机器代码 0x4770
该规则有例外,通常是出于特定原因,但让编译器直接编译为机器代码通常没有意义。你必须有一个目标的汇编器,所以它已经存在,使用它。编译器编写者调试汇编代码比调试机器代码要容易得多。有一些例外,有一种“只是因为我想”,就像你为什么要爬山而不是“因为它在那里”那样到处走。然后是及时的原因,还有其他一些原因。JIT 需要更快地获取机器代码,或者使用一个工具/库/驱动程序/等...所以你可能会看到那些跳过了这一步,开发起来更难。通常你可以通过重命名你的汇编器来测试这个理论(尽管必须点击正确的二进制文件,
所以我们采取我们简单的入门程序
#define FIVE 5
unsigned int more_fun ( unsigned int );
void fun ( void )
{
more_fun(FIVE);
}
编译
arm-none-eabi-gcc -mthumb -save-temps -O2 -c fun.c -o fun.o
arm-none-eabi-objdump -D fun.o > fun.list
第一个 temp 是预处理器获取 #defines 和 #includes 并基本上摆脱它们,生成将发送到编译器的文件
# 1 "fun.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "fun.c"
unsigned int more_fun ( unsigned int );
void fun ( void )
{
more_fun(5);
}
然后调用编译器本身并编译为汇编语言
.cpu arm7tdmi
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.code 16
.file "fun.c"
.text
.align 2
.global fun
.code 16
.thumb_func
.type fun, %function
fun:
push {r3, lr}
mov r0, #5
bl more_fun
@ sp needed
pop {r3}
pop {r0}
bx r0
.size fun, .-fun
.ident "GCC: (15:4.9.3+svn231177-1) 4.9.3 20150529 (prerelease)"
然后调用汇编器将其转换为一个对象,我们可以在这里看到生成的对象的反汇编:
Disassembly of section .text:
00000000 <fun>:
0: b508 push {r3, lr}
2: 2005 movs r0, #5
4: f7ff fffe bl 0 <more_fun>
8: bc08 pop {r3}
a: bc01 pop {r0}
c: 4700 bx r0
e: 46c0 nop ; (mov r8, r8)
现在 bl 0 还不是真实的,more_fun 是一个外部标签,所以链接器必须进来修复这个问题,我们很快就会看到。
more_fun.c 同样的故事
源代码
#define ONE 1
unsigned int more_fun ( unsigned int x )
{
return(x+ONE);
}
编译器输入
# 1 "more_fun.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "more_fun.c"
unsigned int more_fun ( unsigned int x )
{
return(x+1);
}
编译器输出(汇编器输入)
.cpu arm7tdmi
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.code 16
.file "more_fun.c"
.text
.align 2
.global more_fun
.code 16
.thumb_func
.type more_fun, %function
more_fun:
add r0, r0, #1
@ sp needed
bx lr
.size more_fun, .-more_fun
.ident "GCC: (15:4.9.3+svn231177-1) 4.9.3 20150529 (prerelease)"
对象的反汇编(汇编器输出)
Disassembly of section .text:
00000000 <more_fun>:
0: 3001 adds r0, #1
2: 4770 bx lr
现在我们将所有这些链接在一起(它被称为工具链是有原因的,编译、组装、链接一系列链接在一起的工具,一个的输出馈送另一个的输入)
arm-none-eabi-ld -Ttext=0x2000 vectors.o fun.o more_fun.o -o run.elf
arm-none-eabi-objdump -D run.elf > run.list
arm-none-eabi-objcopy -O srec run.elf run.srec
Disassembly of section .text:
00002000 <_start>:
2000: 20001000
2004: 00002021
2008: 00002025
200c: 00002025
2010: 00002025
2014: 00002025
2018: 00002025
201c: 00002025
00002020 <reset>:
2020: f000 f802 bl 2028 <fun>
00002024 <foo>:
2024: e7fe b.n 2024 <foo>
00002026 <dummy>:
2026: 4770 bx lr
00002028 <fun>:
2028: b508 push {r3, lr}
202a: 2005 movs r0, #5
202c: f000 f804 bl 2038 <more_fun>
2030: bc08 pop {r3}
2032: bc01 pop {r0}
2034: 4700 bx r0
2036: 46c0 nop ; (mov r8, r8)
00002038 <more_fun>:
2038: 3001 adds r0, #1
203a: 4770 bx lr
链接器已经调整了外部标签,在这种情况下,通过修改指令以获得正确的偏移量。
4: f7ff fffe bl 0 <more_fun>
202c: f000 f804 bl 2038 <more_fun>
elf 文件格式是一种“二进制”文件,它是二进制的,因为您使用文本编辑器打开它,您会看到一些文本,但大多是垃圾。还有其他“二进制”文件格式,如摩托罗拉 s-record,在这种情况下,它只包括真实的东西、机器代码和任何数据,其中精灵具有调试信息,如字符串“fun”“more_fun”等反汇编程序恰好用来使输出更漂亮。摩托罗拉 S-Record 和 Intel Hex 是这样的 ascii 文件格式:
S00B000072756E2E73726563C4
S113200000100020212000002520000025200000D1
S113201025200000252000002520000025200000A8
S113202000F002F8FEE7704708B5052000F004F858
S10F203008BC01BC0047C04601307047EA
S9032000DC
不再使用太多但并非完全没用,曾经需要这种格式来编程 rom,工具制造商个人偏好他们支持哪些文件格式。二进制文件如何被烧录到微控制器的闪存中?一些工具从主机/开发机器中获取这些位,并通过一些接口和一些软件将其移动到目标,该工具支持哪些二进制文件格式?由谁编写工具来选择一种或多种格式。
早在编译器以各种方式(购买成本和/或将程序保存在计算机上的存储空间,加上中间数据等)都可以负担得起之前,汇编器可以用来制作整个程序。您会看到像 .org 100h 这样的指令,带有“工具链”的汇编器可能具有该功能,但作为链的一部分,汇编器工具需要从汇编语言转换为对象格式,大部分转换为机器代码和其他数据。当然,编译器可以完成所有工作并输出完成的二进制文件,当工具链的一部分时,理智的方法是最终从源代码获取汇编语言。我们习惯的编译工具,gcc、msvc、clang等,除非被告知,否则将为我们生成汇编器和链接器以及编译器,使编译器看起来像是在一个神奇的步骤中从源代码到最终二进制文件。链接器获取某些具有未解析外部标签的单个对象,并决定它们将在内存映像中的哪个位置,在内存中的哪个位置,根据需要解析外部。链接器的功能在很大程度上是这些工具的系统设计的一部分,设计可以使得链接器不修改单个指令,它只将地址放置在约定的位置。一个例子:链接器的功能在很大程度上是这些工具的系统设计的一部分,设计可以使得链接器不修改单个指令,它只将地址放置在约定的位置。一个例子:链接器的功能在很大程度上是这些工具的系统设计的一部分,设计可以使得链接器不修改单个指令,它只将地址放置在约定的位置。一个例子:
向量.s
.globl _start
_start:
bl fun
b .
.global hello
hello: .word 0
乐趣.c
#define FIVE 5
extern unsigned int hello;
void fun ( void )
{
hello+=FIVE;
}
fun.o 反汇编
Disassembly of section .text:
00000000 <fun>:
0: e59f200c ldr r2, [pc, #12] ; 14 <fun+0x14>
4: e5923000 ldr r3, [r2]
8: e2833005 add r3, r3, #5
c: e5823000 str r3, [r2]
10: e12fff1e bx lr
14: 00000000 andeq r0, r0, r0
所以我们可以看到它正在从偏移量/地址 0x14 加载一个数字到 r2 然后该数字用作获取 hello 的地址,然后读取的内容添加了 5 然后 r2 中的地址用于将 hello 保存回记忆。所以 0x14 是编译器留下的占位符,因此链接器可以将地址 hello 放在那里,我们一旦链接就可以看到
Disassembly of section .text:
00002000 <_start>:
2000: eb000001 bl 200c <fun>
2004: eafffffe b 2004 <_start+0x4>
00002008 <hello>:
2008: 00000000 andeq r0, r0, r0
0000200c <fun>:
200c: e59f200c ldr r2, [pc, #12] ; 2020 <fun+0x14>
2010: e5923000 ldr r3, [r2]
2014: e2833005 add r3, r3, #5
2018: e5823000 str r3, [r2]
201c: e12fff1e bx lr
2020: 00002008 andeq r2, r0, r8
0x2020 现在保存了 hello 的地址,编译器构建了程序,使得该地址可以很容易地被链接器填充,并且链接器填充它。当然可以使用分支/跳转地址,以及不同的工具链或不同的工具链来执行此操作来自相同工具的目标会产生不同的解决方案,这通常与指令集有关。你有一个近调用(相对)和远调用(绝对),你编译外部调用远调用所以它总是有效的吗?或者您是否会抓住机会构建一个近距离呼叫并承担链接器必须放入蹦床的风险?
不是那么精确,但我可以让 gcc 很容易地为拇指/手臂做到这一点。
.thumb
.globl _start
_start:
bl fun
b .
.global hello
hello: .word 0
#define FIVE 5
extern unsigned int hello;
void fun ( void )
{
hello+=FIVE;
}
链接二进制文件的反汇编
00002000 <_start>:
2000: f000 f812 bl 2028 <__fun_from_thumb>
2004: e7fe b.n 2004 <_start+0x4>
00002006 <hello>:
2006: 00000000 andeq r0, r0, r0
...
0000200c <fun>:
200c: e59f200c ldr r2, [pc, #12] ; 2020 <fun+0x14>
2010: e5923000 ldr r3, [r2]
2014: e2833005 add r3, r3, #5
2018: e5823000 str r3, [r2]
201c: e12fff1e bx lr
2020: 00002006 andeq r2, r0, r6
2024: 00000000 andeq r0, r0, r0
00002028 <__fun_from_thumb>:
2028: 4778 bx pc
202a: 46c0 nop ; (mov r8, r8)
202c: eafffff6 b 200c <fun>
因为这个特定指令集的工作方式你不能使用 bl (基本上是调用)指令从拇指代码到武装代码,你必须使用 bx 这只是一个分支(跳转)而不是调用,链接器放置了一个蹦床,一些用于我们的代码从一件事跳到另一件事。
并非所有指令集都易于反汇编和/或工具链不包含一个,它不是工具链的必需部分。但是你可以并且应该使用 gnu 和其他工具来重复这个或其他目标,正如你所看到的,我不需要特殊的硬件,我不需要编写十多行代码来查看这些工具的工作。
除了源代码之外都是低级语言。
我相信对象和机器代码指的是同一件事。
没有从源代码到汇编代码的直接转换,因为源代码通常直接转换为机器代码。汇编器可用于将汇编代码转换为机器代码(汇编语言与机器代码具有 1:1 的对应关系)。编译器用于将源代码直接转换为机器代码。
使用汇编程序是因为,由于每种类型的计算机的机器代码不同,因此汇编语言也特定于每种类型的计算机。
高级语言是我们将抽象的低级语言用于易于阅读和理解的代码的语言。它是一种抽象,可以帮助我们在编码时提高工作效率。
低级语言是一种对计算机指令集几乎没有抽象的语言。