Function 函数如何编码/存储在内存中?

Function 函数如何编码/存储在内存中?,function,memory,encoding,cpu-architecture,machine-code,assembly,x86,Function,Memory,Encoding,Cpu Architecture,Machine Code,Assembly,X86,我知道数字和字母是如何用二进制编码的,因此可以存储为0和1 但是函数是如何存储在内存中的呢?我看不出它们是如何存储为0和1的,我也看不出除了0和1之外,还有什么东西可以存储在内存中。函数是由指令组成的,比如或。指令是数字,可以用二进制编码 查尔斯·佩佐尔德(Charles Petzold)的书是对此的一个很好的介绍。事实上,它们作为0和1存储在内存中 下面是一个现实世界的例子: int func(int a, int b) { return (a + b); } 以下是编译器可能为函数

我知道数字和字母是如何用二进制编码的,因此可以存储为0和1


但是函数是如何存储在内存中的呢?我看不出它们是如何存储为0和1的,我也看不出除了0和1之外,还有什么东西可以存储在内存中。

函数是由指令组成的,比如或。指令是数字,可以用二进制编码


查尔斯·佩佐尔德(Charles Petzold)的书是对此的一个很好的介绍。

事实上,它们作为0和1存储在内存中

下面是一个现实世界的例子:

int func(int a, int b) {
    return (a + b);
}
以下是编译器可能为函数生成的32位x86机器指令示例(以称为代码的文本表示形式):

讨论这些指令如何工作超出了本问题的范围,但这些符号(如add、pop、mov等)及其参数都被编码为1和0。显示许多英特尔指令及其编码方式摘要。有关文档/指南/手册的链接,请参见标记wiki


那么,如何将代码从文本汇编转换成机器可读字节(又称机器代码)?例如,指令
addeax,edx
。显示add指令的编码方式。eax和edx是一种称为寄存器的东西,是处理器中用来保存处理信息的点。计算机编程中的变量通常会在某个点映射到寄存器。因为我们正在添加寄存器,并且寄存器是32位的,所以我们选择了操作码00000000 1(另请参见英特尔官方文件,其中列出了所有可用的形式)

下一步是指定操作数。在上一页的同一页中,我们展示了如何使用“addecx,eax”示例来实现这一点,该示例与我们自己的示例非常相似。前两位必须为“11”,以表明我们正在添加寄存器。接下来的3位指定第一个寄存器,在我们的示例中,我们选择edx而不是eax,这给我们留下了“100”。接下来的3位指定了我们的eax,因此我们得到了

00000001 11100000
十六进制是01 D0。类似的过程也可用于将任何指令转换为二进制。用于自动执行此操作的工具称为汇编程序


因此,通过汇编程序运行上述汇编代码会产生以下输出:

66 55 66 89 E5 66 67 8B 55 O8 66 67 8B 45 0C 66 01 D0 66 5D C3
注意字符串末尾附近的
01 D0
,这是我们的“添加”指令。将机器代码字节转换回文本汇编语言助记符称为反汇编:

 address | machine code  |  disassembly
   0:      55              push   ebp
   1:      89 e5           mov    ebp, esp
   3:      8b 55 08        mov    edx, [ebp+0x8]
   6:      8b 45 0c        mov    eax, [ebp+0xc]
   9:      01 d0           add    eax, edx
   b:      5d              pop    ebp
   c:      c3              ret    
地址从零开始,因为这只是一个
.o
,而不是一个链接的二进制文件。所以它们只是相对于文件的
.text
部分的开头

您可以在上看到您喜欢的任何函数(或者在您自己的机器上,在任何二进制文件上,使用反汇编程序,无论是否新编译)


您可能注意到在最终输出中没有提到名称“func”。这是因为在机器代码中,函数是由其在RAM中的位置而不是名称引用的。编译器输出对象文件的符号表中可能有一个
func
项,它引用了这段机器代码,但符号表是由软件读取的,而不是CPU硬件可以直接解码和运行的


有时,我们很难理解计算机是如何在低水平上对这样的指令进行编码的,因为作为程序员或高级用户,我们有避免直接处理指令的工具。我们依靠编译器、汇编器和解释器为我们完成工作。尽管如此,计算机所做的任何事情最终都必须用机器代码来指定。

这是一个非常广泛的问题,答案在很大程度上取决于所使用的语言(以及它是否被编译、解释、转换为字节码、标记……)。我给你一个提示:在JPG文件中,所有文件都以特定的字节序列开始和结束(FF D8和FF D9)。一些序列被编码以特别推断它们在数据中的类型。啊,好吧,太有启发性了!我不理解细节,但我可以看到一个函数是如何仅仅是一系列指令,以及这些指令如何被编码为0和1。嗯,有点。你能扩展一下指令如何被编码为0和1吗?@正如Dougvj所指出的,AdamZerner将操作编码为一些位(操作码)和操作数(数值常量和寄存器名)被编码到其他位。对于操作码,这可能被视为类似于虚拟函数表的索引;对于寄存器,数组索引大致类似。实际的ISA更复杂,因为操作码可以包含隐式操作数(例如,大多数RISC为跳转和链接指令使用隐式指定的链接寄存器,x86 POP隐式使用sp作为寄存器操作数)而且位的数量和位置可能因指令而异。@AdamZerner当然,我大大扩展了这一部分。小心点,它有点毛茸茸的,但我想你会明白的。在每一行包含一个代码块,显示机器代码的十六进制转储以及反汇编的指令助记符+操作数可能会很有用。例如,from
 objdump-drwC-Mintel
。例如,像这样。Godbolt编译器浏览器有gcc/clang/icc输出。我添加了一些我认为是一个改进的东西。这是你的答案,所以请检查并看看你是否喜欢我的更改,如果不喜欢,请回滚或编辑。
 address | machine code  |  disassembly
   0:      55              push   ebp
   1:      89 e5           mov    ebp, esp
   3:      8b 55 08        mov    edx, [ebp+0x8]
   6:      8b 45 0c        mov    eax, [ebp+0xc]
   9:      01 d0           add    eax, edx
   b:      5d              pop    ebp
   c:      c3              ret