汇编程序和C程序的大小几乎相同吗?

4
例如:我创建了一个简单的C程序,打印“Hello, World”,编译后生成了一个大小为39.8Kb的可执行文件。
跟随this问题,我能够创建等效的汇编程序,其大小为39.6Kb。
这让我非常惊讶,因为我预计汇编程序应该比C程序小。正如问题所示,它使用了C头文件和gcc编译器。这会使汇编程序变大吗?还是它们通常大小相近?

使用strip命令,我缩小了两个文件。这样可以去除调试代码,现在两个文件的大小非常相似,都是18.5Kb。

test.c:


2
你为什么期望它们会有很大的不同呢?它们正在做同样的事情。 - Barmar
1
@Barmar 我曾经认为人们(有时)使用汇编语言是因为它更低级、更快,而且产生的输出更小,但如果我错了,请告诉我。 - Xantium
2
编译器在生成最优代码方面非常出色。 - Barmar
4
对于一个非常小的程序而言,其大小受到开销和任何链接库的影响。如果您使用C编译器来编译汇编程序,那么这些可能是相同的。 - Mark Ransom
2
你只是在汇编语言中重写了这个应用程序的一小部分(只调用了printfexit),并将95%的代码实现交给了C运行时库(你可能相当低估了“底层”所做的工作量)。这对于你的汇编版本和C版本来说都是一样的,所以不奇怪你最终得到的可执行文件大小大致相同。最小的Windows PE可执行文件被认为是133字节,我没有检查DOS头区域是否还有足够的空间进行快速而简单的“Hello World”输出,可能没有,但我们可以说200B可能足够了。剩下的39kB是方便和C运行时库。 - Ped7g
显示剩余20条评论
3个回答

6
如果你手写的代码与编译后的函数相当,那么它们的大小肯定是相似的。它们正在执行相同的操作,如果你能与编译器竞争,你的结果将会是一样或者相似的。现在,你所看到的文件大小表明你完全关注错了地方。 虽然该文件被称为二进制文件,但它包含了大量其他内容。在这种情况下,你需要比较的是函数的大小和机器代码的大小,而不是容纳函数、调试信息、字符串和其他许多东西的容器的大小。 你的实验有缺陷,但结果大致上还是符合预期的。但前提是你以相同的方式生成代码。这种可能性很小,因此说除非你用相同的方式生成代码,否则你不应该期望得到类似的结果。 让我们来看一个简单的函数。
unsigned int fun ( unsigned int a, unsigned int b)
{
    return(a+b+1);
}

同样的编译器生成了以下内容:
00000000 <fun>:
   0:   e52db004    push    {r11}       ; (str r11, [sp, #-4]!)
   4:   e28db000    add r11, sp, #0
   8:   e24dd00c    sub sp, sp, #12
   c:   e50b0008    str r0, [r11, #-8]
  10:   e50b100c    str r1, [r11, #-12]
  14:   e51b2008    ldr r2, [r11, #-8]
  18:   e51b300c    ldr r3, [r11, #-12]
  1c:   e0823003    add r3, r2, r3
  20:   e2833001    add r3, r3, #1
  24:   e1a00003    mov r0, r3
  28:   e28bd000    add sp, r11, #0
  2c:   e49db004    pop {r11}       ; (ldr r11, [sp], #4)
  30:   e12fff1e    bx  lr

并且这个

00000000 <fun>:
   0:   e2811001    add r1, r1, #1
   4:   e0810000    add r0, r1, r0
   8:   e12fff1e    bx  lr

由于不同的设置,指令数量为13条而不是3条,大小超过4倍。

一个人可能会直接从C语言中生成这个,没有花哨的东西。

add r0,r0,r1
add r0,r0,#1
bx lr

我不确定在运算顺序中是否需要在将该总和加入a之前先将1添加到b中。或者如果这并不重要。我从左到右进行操作,而编译器从右到左进行操作。
因此,你可以说编译器和我的汇编生成了相同数量的二进制字节,或者你可以说编译器产生了大约4倍于此的东西。
将上述内容扩展为真正执行有用操作的程序。
请给读者(即OP,请勿泄露)练习,以了解为什么编译器可以生成两个不同的正确解决方案,但大小如此不同。
编辑
.exe、elf和其他“二进制”格式中提到的调试信息、包含函数/标签名称的ASCII字符串可用于创建漂亮的调试屏幕。它们是“二进制”的一部分,因为它们是负担的一部分,但不是机器代码或执行该程序时使用的数据,至少不是我提到的那些东西。你可以使用编译器设置而无需更改程序所需的机器代码或数据,来操纵你的.exe或其他文件格式的大小,因此,相同的编译器-汇编器-链接器或汇编器-链接器路径可以通过包含或不包含这种附加负担来制作二进制文件。因此,这是理解文件大小及为什么即使你的hello world程序大小不同,文件也可能大致相同大小的一部分。但是如果我理解你的问题,那么你想知道的是,这10个字节在编译和手写C之间如何进行比较。
还要注意,编译器是由人制作的,因此它们产生的输出与至少那些人可以产生的输出相当,其他人可能做得更好,许多人则根据您对好和坏的定义而定。

1
这个大尺寸绝对与编译器无关,而是与 CRT 如何链接到程序有关。如果使用静态链接,则会将 CRT 代码的大部分链接到程序中。如果使用 DLL(msvcrt.dll)和 C 运行时,则即使是 C++ 代码的大小也会接近 2500 字节。 - RbMm
静态与动态是其中的一部分,但这与编译器与手写汇编无关。您可以使用这些链接器功能来使其变得更大或更小,以及调试信息和其他负担。 - old_timer
crt通常相对于程序的其余部分非常小。当然,这取决于程序。但是,与静态和动态不适用于此问题类似,main()和main:可以具有相同的负担。它们不是编译为asm与手写asm之间的差异的一部分。 - old_timer
编译后的代码,使用大多数主流工具在链接器之前会经过汇编器。因此,很明显编译后的代码和手写代码可能有相同的负担,而这个负担并不是问题的一部分,就我所理解的而言。如果忽略编译后的代码和手写代码的区别,比较为什么一个.exe文件与另一个文件大小不同,这才是整个问题。请发表您的答案,以便OP有机会更改选择的内容。负担或编译后的代码与手写代码的区别。 - old_timer
请发布您对此答案的替代方法,并让OP决定哪个适用于问题。同时阅读问题的标题以及问题本身,OP显然不知道“二进制”中有什么,所以问题是关于二进制中有什么还是标题,编译与手写汇编之间的区别。请发布您的答案,以便我们所有人都可以看到它。 - old_timer
显示剩余2条评论

5

代码大小约为39 KB,与使用的编译器和语言无关(C/C++ASM),不同的优化、调试信息等可能会使这段小程序的大小变化,但不会超过1000字节。我将用此程序进行测试构建。

#include <Windows.h>
#include <stdio.h>
void ep(void*)
{
    ExitProcess(printf("Hello, World"));
}

链接器选项:
/INCREMENTAL:NO /NOLOGO /MANIFEST:NO /NODEFAULTLIB 
/SUBSYSTEM:CONSOLE /OPT:REF /OPT:ICF /LTCG /ENTRY:"ep" /MACHINE:X64 kernel32.lib msvcrt.lib

他得到了大小为2560字节的x86/x64 exe文件。

有什么不同吗?在于/NODEFAULTLIB和我的版本中的msvcrt.lib——它是纯导入库。

其余的35kb+大小是由于使用静态链接c运行时所引起的。即使你用汇编语言编写程序,你也需要使用一些库来链接printf。而你的库包含了一些与你的代码静态链接的代码。这就是这35kb的原因。

任务不是c++对asm的比较——这里没有区别。任务是使用c-runtime或者不使用它。


4

我同意old_time的观点,但我也进行了一项快速的测试来获得基准数据。使用VS-2017 Pro编译后,如果我查看调试输出文件夹,可得到类似的结果(约37KB);但是在发布版本中,它更接近于9KB。其中很大一部分差异在于需要调用操作系统/C运行时DLL的静态库的大小。

编辑:尽管大多数现代C编译器可以匹配或超越大多数手写汇编代码,但手写汇编代码之所以可以更小,是因为它不必具备所有C运行时的开销,但这种差异很少足以证明汇编器代码的额外开发和维护成本,特别是对于非平凡应用程序而言。现代操作系统内核主要使用C或其他高级语言编写,并仅在少数关键函数中使用针孔汇编优化,这也是有原因的。

简单的“hello world”类程序并不适合比较C和汇编。因为编译器或人类在优化方面没有太多机会。编写一个数学或数据处理库和应用程序并进行比较。我敢打赌编译器会胜过你。


是的,你说得对。我刚刚发现使用“strip”命令可以将大小减小到9.5Kb。 - Xantium
@Simon,即使在那个时候剩下的大部分仍然是那些必须要访问操作系统API的静态库。 - jwdonahue
使用汇编语言,如果你真的努力去做,你可以为微不足道的程序减少可执行文件大小。例如,http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html:一个只是退出的Linux ELF可执行文件可以被压缩到42个字节(将机器码放在ELF程序头中的一些不重要的字段中)!在Windows上,由于不能直接使用系统调用,你无法避免动态链接一些DLL,因此你不能制作直接进行系统调用的小型静态可执行文件(除非采用hack方法)。 - Peter Cordes
@jwdonahue: 没错;在只能在Linux或Windows这样的大型操作系统下运行的代码中,执行文件大小优化通常不值得投入。但是,你可以更容易地使用汇编语言创建小的静态可执行文件,而不是C语言。正如您所说,在现实生活中,除了引导加载程序或微控制器外,通常情况下并不值得这样做。 - Peter Cordes
我必须承认,我已经老到记得那个时候,即使是在嵌入式系统上,我也能够生成比任何可用编译器更小/更快的机器代码。然而,今天,即使是后者也大多是成熟的工具链的商品设备,我不再为我所使用的每个微控制器维护汇编宏和子程序库。除了 Windows 内核中几行编译器内部函数之外,我怀疑是否还有任何一个产品在其中运行我的汇编代码。 - jwdonahue
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接