① 编译器生成的汇编语句执行顺序为什么与C代码顺序不同
不影响语义的前提下编译器可以任意重排代码顺序;
在乱序执行(Out-of-Order)的CPU里,机器码的执行也可以不按照你在“汇编”层面上看到的顺序执行,只要不影响语义。
所以说这些中间步骤的顺序,作为底层细节平时不需要那么在意——它们多半跟原始源码的顺序是不一样的。
现代优化编译器优化的思路之一是“基于依赖的优化”(dependence-based optimization)。题主引用的CSAPP的例子:
int arith(int x, int y, int z) {
int t1 = x + y;
int t2 = z * 48;
int t3 = t1 & 0xFFFF;
int t4 = t2 * t3;
return t4;
}
所有涉及运算的值都是局部标量变量(local scalar variable),这是最便于编译器做分析的情况,所有依赖都可以显式分析。
由于整个函数没有分支,这里也不需要讨论控制依赖(control dependence),只要讨论数据依赖(data dependence)就好。
把数据依赖图画出来是个DAG(这里正好是棵树,特例了):
x y z 48
\ / \ /
t1 0xFFFF t2
\ / /
t3 /
\ /
t4
优化必须要满足的约束是:每个节点求值之前,其子节点(依赖的数据源)必须要先求了值。
显然,t1和t2之间没有依赖关系,它们的相对求值顺序怎样重排都没关系。
有本我很喜欢的书,里面讲的是各种基于依赖的优化:Optimizing Compilers for Modern Architectures - A Dependence-based Approach
以上是理论部分。
================================================================
下面来看例子。
我们可以用一个实际编译器来看看CSAPP的例子编译出来的结果:
.text
# -- Begin arith
.p2align 4,,15
.globl arith
.type arith, @function
arith:
.p2align 4,,7
/*.L0:*/ /* Block BB[54:2] preds: none, freq: 1.000 */
movl 8(%esp), %edx /* ia32_Load T[139:10] -:1:22 */
addl 4(%esp), %edx /* ia32_Add Iu[141:12] -:2:14 */
movzwl %dx, %edx /* ia32_Conv_I2I Iu[142:13] -:4:15 */
imull 12(%esp), %edx /* ia32_IMul Iu[143:14] -:5:15 */
leal (%edx,%edx,2), %eax /* ia32_Lea Iu[144:15] -:5:15 */
shll $0x4, %eax /* ia32_Shl Iu[146:17] -:5:15 */
ret /* ia32_Return X[152:23] -:6:3 */
.size arith, .-arith
# -- End arith
这里用的是libFirm。可见它跟CSAPP书里所说的汇编的顺序又有所不同。这也是完全合理的。
这个编译结果的顺序是:
edx = y;
edx += x;
edx = zeroextend dx; // edx = edx & 0xFFFF
edx *= z;
eax = edx * 3;
eax <<= 4; // eax = eax * 16
也是完全符合依赖关系的约束的一种顺序。
之所以用libFirm举例是因为它的中间表示(Intermediate Representation)是一种程序依赖图(Program Dependence Graph),可以很方便的看出控制与数据依赖。把CSAPP那里例子对应的libFirm IR画出来,是这个样子的:
(这张图跟我前面画的数据依赖图正好是左右翻转的,不过意思一样。(这张图跟我前面画的数据依赖图正好是左右翻转的,不过意思一样。
Arg 0、1、2分别代表x、y、z。白色方块是普通数据节点,黄色方块是常量节点,蓝色方块是内存相关节点,红色方块是控制流节点,粉红色方块是特殊的开始/结束节点。)
某版LLVM生成的代码:
; MoleID = '/tmp/webcompile/_16355_0.bc'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-ellcc-linux"
; Function Attrs: nounwind readnone
define i32 @arith(i32 %x, i32 %y, i32 %z) #0 {
entry:
%add = add nsw i32 %y, %x
%mul = mul nsw i32 %z, 48
%and = and i32 %add, 65535
%mul1 = mul nsw i32 %mul, %and
ret i32 %mul1
}
attributes #0 = { nounwind readnone "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.ident = !{!0}
!0 = !{!"ecc 0.1.10 based on clang version 3.7.0 (trunk) (based on LLVM 3.7.0svn)"}
最终生成的x86汇编:
.text
.file "/tmp/webcompile/_15964_0.c"
.globl arith
.align 16, 0x90
.type arith,@function
arith: # @arith
# BB#0: # %entry
movl 8(%esp), %eax
addl 4(%esp), %eax
movzwl %ax, %eax
imull 12(%esp), %eax
shll $4, %eax
leal (%eax,%eax,2), %eax
retl
.Ltmp0:
.size arith, .Ltmp0-arith
.ident "ecc 0.1.10 based on clang version 3.7.0 (trunk) (based on LLVM 3.7.0svn)"
.section ".note.GNU-stack","",@progbits
GCC 4.9.2 x86-64:
arith(int, int, int):
leal (%rdx,%rdx,2), %eax
addl %edi, %esi
movzwl %si, %esi
sall $4, %eax
imull %esi, %eax
ret
Zing VM Server Compiler x86-64:
# edi: x
# esi: y
# edx: z
movl %edx, %eax
shll $0x4, %eax
leal (%rsi, %rdi, 1), %ecx
shll $0x5, %edx
addl %edx, $eax
movzwl %ecx, %edx
imull %edx, %eax
② 对单片机进行编译用汇编和用c语言的区别
我的见解,汇编与C的本质区别是:效率
效率有两层含义:
1. 写或修改或维护的快(程序员的效率)即:相同时间里,程序员写更多的代码;
2.运行的快。即:相同时间里,CPU执行更多的、有效的运算。
那么,你认为哪个更重要呢?
重要与否,也是随环境变化而变化的,比如:要想“相同时间里,CPU执行更多的、有效的运算”的另外一个方法是:提高CPU主频。目前,得益于IC设计的先进工艺及技术进步,单片机的主频比以往有大幅提高,而人力成本又在提高,所以,似乎C是更好的选择。但我在C语言里,嵌入了汇编,用以处理模拟I2C总线的时序及20通道的AD并行采样及计算处理,甚至用汇编重写memmove()。
总结:他们的区别是使用它们的依据,也是它们各自无法完全替代的原因。至于什么高级低级之分,我不发表看法。
③ c语言中如何调用汇编程序
1、如果汇编程序是可执行文件,比如exe文件,则可以使用system函数直接调用。比如下面的代码,用system()打开windows上的记事本程序。
#include
#include
int main()
{
system("notepad.exe");
return 0;
}2、在C语言源码中,可以通过内联汇编来直接编写汇编程序代码。不同的编译器使用内联汇编的方法不同,vc/vs编译器中一般使用__asm关键字来使用内联汇编,gcc编译器一般使用asm关键字来使用内联汇编,以vc6.0为例,下面的代码通过使用内联汇编来计算1+1,并将结果保存到int型变量result中。
#include
int main()
{
int result;
_asm {
mov eax,1
mov ebx,1
add eax,ebx
mov result, eax
}
printf("1+1=%d\n", result);
return 0;
}
④ c语言编译器是用汇编语言写的吗
这个是肯定的。算法优化,首先是逻辑描述的精炼化。至于C,只是计算逻辑到计算机模式的一种映射,而汇编仅是利用特殊计算机指令的一个更深的藕荷。
不过有一点,C语言由于是计算逻辑到计算机模式的映射,所以不单单考虑算法本身,还肩负数据组织的实现。数据流动方式,数据组织方式,对计算性能的影响也很大。这要看是否和计算机组成原理相贴近。其实这块也是侧重逻辑的设计,而不是具体机器指令的实现,因此汇编是无能为力的。
不过在DSP等特殊CPU架构,C语言和编译器无法很好的将上述逻辑转换为机器指令,或者C本身的逻辑无法很好的贴近CPU的特性,那么还是得汇编。一个典型的例子就是如何使用DSP的并行指令(通常的矢量计算)和并发指令集,几个不同的指令(隶属不同处理单元)的同时执行。使用C语言无法描述清楚这些逻辑方式,而编译器又太水,则还是不得不用汇编。此时C语言仅能沦落到大的计算机组织特性的贴近,和整体框架,模块的设计上。细节方面无能为力了。
⑤ c语言编译器如何运行
编译共分为四个阶段:预处理阶段、编译阶段、汇编阶段、链接阶段。
1、预处理阶段:
主要工作是将头文件插入到所写的代码中,生成扩展名为“.i”的文件替换原来的扩展名为“.c”的文件,但是原来的文件仍然保留,只是执行过程中的实际文件发生了改变。(这里所说的替换并不是指原来的文件被删除)
2、汇编阶段:
插入汇编语言程序,将代码翻译成汇编语言。编译器首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言,同时将扩展名为“.i”的文件翻译成扩展名为“.s”的文件。
3、编译阶段:
将汇编语言翻译成机器语言指令,并将指令打包封存成可重定位目标程序的格式,将扩展名为“.s”的文件翻译成扩展名为“.o”的二进制文件。
4、链接阶段:
在示例代码中,改代码文件调用了标准库中printf函数。而printf函数的实际存储位置是一个单独编译的目标文件(编译的结果也是扩展名为“.o”的文件),所以此时主函数调用的时候,需要将该文件(即printf函数所在的编译文件)与hello world文件整合到一起,此时链接器就可以大显神通了,将两个文件合并后生成一个可执行目标文件。
⑥ c语言编译器是用汇编语言写的吗
直接用指令码写第汇编语言编译器用汇编语言写新编译器其实语言都写汇编编译器 比第C语言编译器能用汇编写C编译器都用C语言写神奇吧哈
⑦ 汇编语言与C语言有什么区别
1、操作复杂程度的不同
c语言,与汇编语言相比,c语言在更加接近人的一般思维,因此在程序的设计过程中比较容易操作,此外在进行一些复杂的操作,运算时,c语言比汇编就要简单很多,尤其是c语言中的丰富的函数库,可以直接实现一些原本很复杂的功能,并且从代码量来说任意一个c语言程序,通过反汇编之后变成汇编语言程序,其长度都可能要增加好几倍。
2、使用范围的不同
c语言程序的事件将会只是编写汇编语言程序的几分之一,从编写程序的效率上来说c语言无疑更高,此外C语言是高级程序语言因此可移植性较好,不太受到到硬件设备的限制。
在实现一个功能时,汇编语言可以直接奔着目标去,而C语言则是给你提供了一种对于对于这种问题的普遍处理办法,不具有针对性,因此会有许多多余的在这个问题中不需要的过程,因此可能回事程序较大,运行较慢。相对与汇编语言,C语言更加适合一些较大型项目的开发。
3、运行的速度和效率不同
程序没有了汇编语言计算机直接就无法运行,因为汇编语言是基于计算机底层硬件的编程,通过它实现了对cpu,内存,硬盘以及外界设备的直接操作,因为直接所以汇编语言在程序的大小,执行的速度与效率方面几乎无可比拟,但是也是因为直接,所以汇编程序难以移植,且完成相同的操作代码量太大,在进行一些大的项目是,单独使用汇编进行编程几乎不可能实现。
⑧ c语言编译器是用什么于语言写的
第一个C语言编译器应该是用汇编写的,但是第一个成熟的C语言编译器应该是由汇编和C语言共同写的。
编译原理讲到了“自举编译器”。大意就是先用底层语言(应该是汇编)写一个能运行,但效率极低的C语言编译器(底层语言不好优化),有了C语言的编译器以后,就可以用C语言好好写一个编译器了,用之前那个运行没问题,但效率低得编译器编译一下,就得到了可以使用的编译器了。
⑨ VC++6.0编译器是如何编译C语言程序的
编译过程如下:
1.预处理阶段,主要是宏替换和库的引入
2.汇编阶段,将1步骤的.c文件通过汇编器生成汇编文件.asm
3.编译阶段,将2步骤的文件通过c编译器,生成目标文件.obj
4.链接阶段,将3步骤的.obj文件通过链接库和其他目标文件,生成可执行文件.exe
⑩ 从C语言源程序到汇编语言程序,C语言编译器完成何种处理
编译器就是把高级语言转换成机器语言,就是二进制代码。
汇编语言可以说是机器语言的助记符,转换比较容易,基本上就是二进制直接替换。
高级语言就复杂了,转换起来很是麻烦,并且需要优化,所以编译器是个大的软件工程。