adwin's blog
本科生学C语言的心得,兼议“C是最通用和最底层的宏汇编语言”
post by:adwin 2012-1-25 15:22

/*注意了,转的,我不是本科生。。。*/

/*在CSDN看到的,见文章写的实在很好,令我爱不释手,于是乎转来了*/

/*下面开始正文*/

pochioly2008 @AT@ gmail.com,遵守CC协议。转载请注明地址谢谢。
http://blog.csdn.net/pochioly/article/details/6917659

本人是计算机专业的普通本科生,关于语言和相关的东西积累多了肯定也有不少思考,希望与大家交流,不足之处还请各位能有空指点斧正。

1. 关于标题
2. 语言
a) 符号,存储,可见性或者作用域
i. 符号
ii. 存储性
iii. 局部存储
iv. 字面量和POD
b) 流程控制
i. 控制结构
c) 函数,调用约定,外部符号
d) 接口
i. 相互调用与内嵌汇编
ii. ABI
iii. 标准输入输出
e) 杂项
i. 预处理指令
ii. 宏


本科生浅谈学C语言的心得,兼议“C是最通用和最底层的宏汇编语言”
1. 关于标题。
我尽量保持谨慎,并以此为荣。不过这次在标题上就失败了,定义上来讲,严肃的汇编语言是直接对应机器码的助记性的指令。以前作为严肃的汇编语言论者,我会觉得最底层的是严格的汇编语言,最通用的可能也是严格的汇编语言,不过现在我失望的发现两者都不是。严格汇编语言能接触到的底层,没有C是接触不到的;而通用性上来讲,汇编语言从平台限定这一点就已经失去了很多优势。这两点在后面的讨论中详述。
另外,我曾经以为C是一种宏汇编语言,但是现在越觉得现在主流的宏汇编是参考了C这种类型的语言。宏汇编家可能会不高兴,他们会认为汇编不会向C学习。这里也不打算谈历史原因,因为如果楼主自己有一点疏忽,这可能会讨论到一些不愉快的话题,例如从BCPL刚脱胎的C和某某机器(非intel386结构,很可能还是大型机,而且跳转指令(Jxx)叫做分支指令(Bxx))的汇编指令进行比较,这个也毫无意义,却会令人想到类似于脚本社区的一些不愉快的争论。语言是自己能说明的。
下面就常见的主题(其实可能只是楼主熟悉,有些并不常见)讨论一下语言:
2.语言
符号,存储,可见性或者作用域
a)符号:
常见的符号在汇编里面是变量地址、可调用地址或者可跳转地址。对应C可以是变量,函数以及标号。之所以说“可以是”,因为gcc也出现了goto *func这种语法,其中func是函数名,并且在一些底层的实践中似乎没什么用,所以我不得不把它包含进来。
其实这点不能算最底层的。机器语言代码这三者和其他的是没有区分的,这些意义和段属性也是根据需要附加上去的,严肃汇编语言也好C也好,都是最接近这种底层的某种方式。
b)存储:
我曾经试着写了一个极其简陋的编译器,连文法都害怕自己去分析,好在简陋有简陋的过法,不引起歧义的情况下,只好求助于现实高级语言的正则表达式,(当然如果那时候我会一点Lex+Yacc/Flex+Bison可能会稍微看起来好点)。 然而有一样东西没有工具可以给你借鉴,(除非你就是改别人的编译器源码),那就是存储。除非你打算一直用寄存器,要不然总得和栈、堆、或者类似的东西打交道。想想看:你有了文法分析器,你有了gcc工具链,(C形式的预处理和汇编链接过程是很容易完成的),你还得定义存储。
好在汇编和C都是任意位置访问的,这两者都可以轻而易举定义存储。C常见的做法是全局存储的地址编译时分配(这个分配是assign,相比于allocate而言);局部存储的地址编译时assign,运行时在栈上面allocate;至于动态的分配这个常见由操作系统支持,程序员自己完成。这点汇编语言上还是没有任何优势,乃至于MASM不得不使用local伪指令来帮助它的入门用户来完成局部变量。当然严肃的汇编语言家可以在汇编上规定一个“闭包存储”等等,如果那样我可以试试在C里面用数据结构来完成同样的事情。
c)局部存储:
局部存储这一现象不是C独有的,而且在某些机器和配置上,由于程序的局部性以及缓存等,最靠近的栈帧可能会直接得到优化, 相对于变量全局化单独放在段里面可能还更有优势。作为曾经的严肃汇编语言用户(开选项-pedantic的),我甚至觉得.local伪指令这种不够酷,例如C代码
void func(){
if(…){ }
if (…){
for(…){int x;}
}
}
中的局部变量x
我认为应该叫做 func_2_1_x
并宏定义为 $-xx(%ebp)(xx自己计算一下就好了)。
而且需要手动去sub $yy, %esp(同样yy自己计算一下就好了)
d)字面量和POD
机器语言是没有non-trivial的字面量的,一切都可以当作字面量,或者一切都不是。严肃的汇编语言却需要借助语言以外的方法来表示一个字面量(我听见有人在窃笑说 DB ‘Hello world’, 0D, 0A, 不过DB也是伪指令可别忘了),GNU as更有一个.asciz伪指令,表示一个字符串字面量以0结尾,例如.asciz “Hello”其实是’H’ ‘e’ ‘l’ ‘l’ ‘o’ ‘\0’,这还是很体现基于C系统的字符串特点,要不然为什么不用’$’表示字符串结尾呢?相比之下C的字面量还算丰富,1, 1.0f, 1.0, 1.0e+123, “Hello”, ‘H’, 还有初始化数组的int a[]={1,2,3}的{1,2,3},或者结构体的struct node a={1,2,(struct node *)0} 等等。至于有些语言,复杂的对象乃至于函数都可以表示成字面量,我虽然羡慕,不过好像与本篇无关就略去。
POD全名Plain-Old-Data,又名概念的天外来客。简而言之就是一个类似于结构体或者联合体的东西,按字节被复制到另一个内存区域的时候,可以按照原来被复制的那个东西一样使用。因为我们只是讨论C与严格汇编与宏汇编的话,是看不见这个东西的,C++为了兼容C语言时和C的结构体有所区别才引入了POD的概念。代表例子是struct和union以及他们的各种形式的杂交,严格汇编是不需要这些概念的,访问那个内存地址,操作他,不需要那些表示struct或者union的伪指令。

2. 流程控制。
我们学程序有三种结构,顺序选择循环,不是只有C才有这样的结构,编译原理课上讲正则表达式等价于正则文法等价于有穷自动机。顺序结构相当于正则表达式的自然连接,选择结构就是正则表达式的选择符号,循环结构就是闭包或者说克林星号(*)。严格的汇编用分支语句老老实实控制流程,而宏汇编家用.if .while来实现,好吧,即便不是模仿C,也算是模仿某种类Pascal语言好了。而且因为.if后面是表达式,宏汇编又不得不引入简单的表达式机制,从这点上来讲,C确实很像宏汇编家用的那种语言。
关于任意地址跳转的那种能力,我会在稍后来讨论,现在我们知道这个放在C里面是有用的,在现在版本的GNU C里面至少我们可以void *a[4]={&&label1, &&label2, &&label3, 0x12343210}; goto *a[N];

3函数,调用约定,外部符号
我个人而言不喜欢MASM的.invoke伪指令,习惯于直接push或者mov参数然后call,
然后看看情况调整栈帧指针。别忘了严格的汇编其实是不需要非要用栈传参的------可以只用寄存器,事实上,只要你愿意,C也是可以用寄存器传参数的。这意味着C可以调用non-trivial或者说一般的汇编程序的代码,但是汇编程序为了在POSIX和Ansi C的系统下重复使用,却不得不遵守C的调用约定以及字符串惯例( (unsigned char)0或者(unsigned short)0或者其他这样的)结尾。C和用汇编代码写成的程序,形成机器码以后,生成的外部符号从这个角度讲并没有多少不一致的地方,从某种意义上讲,你调用的对象,传参像C的风格,返回值像C的风格,寄存器维护惯例像C的风格,为什么一定要认为他是汇编的而不能是C的呢?

4.接口
a)相互调用与内嵌汇编
就像VB可以调用C的DLL,而Java可以调用JNI接口的动态链接库一样,也不能说他们就是C。C内嵌汇编之后,也不能就说是汇编语言,然而却可以当作宏汇编。如果宏汇编家非要说C的这种方式不纯粹,那需要指出汇编代码也有因为汇编器太老而不得不写出DW 0x310F这种用伪指令写出的机器码的时候。既然如此何必苛求C的编译器能产生的指令集呢? 内联汇编当然可以是一种宏汇编形式。顺便提一下,用GNU C的同好,如果没有用过这种形式的操作,不妨一试:int register i __asm(“eax”)=1.0;
b) ABI
ABI就是Abstract Binary Interface。我们常说,C没有标准的ABI,而汇编有任意的ABI,从这点上讲二者是几乎一致的,不过这实在不能作为观点大概。
c) 标准输入输出
不小心拿了Ansi C和POSIX的东西来说事。不过真的有人打算用int $0x21来输出DOS字符串?那个字符串结尾的字符已经不能移植了。相比之下,就算缺少必要的二进制接口,(标准输入输出通常是文本,但是似乎可以是二进制的?),程序仍然可以通过标准输入输出通信。




5杂项
预处理与宏
Ansi C和常见的宏汇编都有自己的预处理宏系统,其实两方我都建议可以用其他的比如m4之类的的工具,出于某些特定的需求的话。当你意识到生成编译或者汇编代码之前都是文本处理的时候,两种语言的区别就会在一个很清澈的层面展示出来。我曾经做过这样的事情,为了调用一个C++写的虚成员,当然也许不是最好的实现,不过当时是一个只有C而且不能链接外部静态库的实现:
#define Thiscall(...) do{ \
__asm__("push %ecx\n\tmov __cxxpThis, %ecx"); \
__VA_ARGS__; \
__asm__("pop %ecx"); }while(0)

#define this(var) do{ \
void *ptmpthis = __cxxpThis;\
__cxxpThis = var; \

#define __call(...) \
Thiscall(__VA_ARGS__); \
__cxxpThis = ptmpthis; }while(0)
然后这个设施就形成了
int (__stdcall *vmember)();
*(int *)&vmember = (int *)(*(int *)ppInput->ppThis)[2];//这两行的vmember, this和2仍然可以写成宏
this(ppInput->ppThis)__call(retv = vmember( filename ) );
如果是宏汇编,你会怎么做呢?如果你的编译器将thiscall当作其他的传参方式,面对这个程序还是要mov <this指针>, %ecx对吧,但是这以外其他的地方C程序我仍然可以用C的方式进行调用。当然ecx可以被重新分配,实际上如果括号里面的表达式够复杂的话,这里应当写成gcc的寄存器破坏内联汇编形式为好。
当然这里用了变量保存ecx不太好,习惯上可以用esi来保存ecx的值,这里宏汇编可以用自定义宏写出很短的代码,C也可以,相比之下,我不认为宏汇编家应该拿向C或者其他高级语言学的东西来表明宏汇编比C更好。
毕竟PC上我们在Ring3编程的话,C风格的系统调用才是底层,其他的都只能算是调用他们的实用代码。在其他方面比如嵌入式的系统,我不知道是怎样的,不过我觉得C仍然会是主要和通用的语言,有人可能会认为嵌入式对时间空间要求比较严格,因此比较相信手写汇编代码,我只好说,编译器对某些特定情景的优化比手动优化的还好,就算编译器一时不能实现某种优化,那么我们仍然可以从语言以外的东西出发,例如gcc工具链的链接脚本。而且我并不是说只能用一种语言,完全可以让效率的部分用纯汇编来写,而构架的部分用C来调用,C是一种粘合性的中间语言,缺少了汇编语言,仍然可以用C去实作目的机器的机器码,而缺少了C和C的一系列标准、约定和惯例,汇编和高级语言(如嵌入式平台的Java)的连接就未必能这么方便,从这个意义上,C才是最基础和最通用的语言。
另外BOOST::PreProcessor还是很不错的,个人觉得,它不仅仅是C++中有用(也可以说C++正常的用法可能用不到他),B.Stroustrup大牛也说过C的预处理和宏机制,应该慢慢从C++中淡化,成为独立于语言的一部分。

评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容