tony --- C语言那些事儿(1)

本文通过一个个实际案例程序来讲解C语言.

研究案例一

工具: Turboc C v2.0,Debug,MASM v5.0,NASM
实例C程序:
/* example1.c */
char ch;

int e_main()
{

e_putchar(ch);

}

目标内容:C语言调用函数的方法与细节

我们使用的C编译器是16位的Turboc C v2.0,它生成的是16位的代码,比较简单,方便我们来研究.同时我们也需要用到DOS下的DEBUG来进行反汇编.由于我们很多案例中的程序并不是完整的C程序,所以Turboc下的Tlink并不能为我们生成目标程序,所以我将使用MASM中的link.exe,同时里面的exe2bin.com也可以为我们把exe文件转换成bin文件.

这个程序没有main函数,我们用e_main来代替main函数.这样我们能避开C语言对main函数进行一系列处理的代码.同样,我们也用e_putchar()来代替我们平常使用的putchar().这里"e"的意思就是"example".

没有了main函数,我们的C程序就没有了入口,所以在开始编译这段C代码之前,我还得写几行简单的汇编代码,通过它来作为我们程序的入口.

; C程序的入口 start.asm
[BITS 16]
[global start]
[extern _e_main]
start:
   call _e_main

按照C语言的习惯,所以C总的名词都要自动在前面加一个"_"下划线.所以,我们在C中的e_main函数,如果要在汇编中调用,就变成了_e_main函数.这段汇编代码只有一句:call _e_main,就是调用我们在C中的e_main函数


这段代码我将用nasm来进行编译.生成start.obj
nasmw -f obj -o start.obj start.asm

下面我们用Turboc C来编译这段C代码:
TCC -mt -oexample1.obj -c example1.c
link start.obj example1.obj,example1.exe,,,
exe2bin example1.exe
这样,我们就得到了这段C代码编译出来的机器代码文件(example1.bin)了.
下面我们用DEBUG这个老DOS的工具来对example1.bin进行反汇编.

DEBUG
-n example1.bin
-l 0
-u 0
xxxx:0000    CALL 0003
xxxx:0003   MOV   AX,000B
xxxx:0006   PUSH AX

xxxx:0007   CALL 0020
xxxx:000A   POP   CX

这里看到蓝色的代码就是我们整个C程序的所生成的代码了.
最开始的第一句CALL 0003是我们用nasm编译的start.asm所生成的代码.
我们主要目标是研究蓝色的C语言的代码,第一句start.asm所生成的代码太简单,就是调用e_main函数.而我们的e_main函数就是蓝色代码部分.

从C源程序中我们看到,我们在e_main做的就是一件事情:调用e_putchar(ch);其中ch是传给出e_putchar的参数.

MOV AX,000B 
000B就是我们的全局变量ch所在内存的地址.C语言会把所有的全局变量在另一块内存区.C代码先把ch的地址传给AX,然后通过
PUSH AX
把AX的值,也就是ch的地址压入堆栈.然后再
CALL 0020
而0020就是e_putchar代码的地址.通过这跳语句,计算机就跳到e_putchar的代码部分去执行了.我在这里并不给出e_putchar的代码,因为我们这个案例只是研究C语言中如何传递参数给其它函数的,并不管e_putchar如何取参数.下在一个案例中,我们将研究函数如何取参数.

在这里我得把CALL指令解释清楚,因为在下个研究函数如何取参数的部分中大家可能会迷惑.CALL XXXX 指令简单地或就是
PUSH IP
JMP XXXX
它首先把当前的执行地址IP压入堆栈,然后跳转到要CALL的地址去.CALL和RET指令是配套的.RET指令等同于
POP IP
也就是回复CALL前的执行地址IP.
正因为这样,所以你一旦使用了CALL指令,你的堆栈指针SP就会自动减2.

POP CX
是每个函数调用完毕后都有的必备操作.在这里它不起任何作用.可能唯一的作用就是与CALL 0020前的PUSH AX像对应.这样堆栈指针SP才能回原.

好了,简单的第一个案例研究结束了.虽然就这4跳指令,但是我们已经可以看出C语言传递参数方法了.总结起来就是
通过"MOV AX,参数地址"把参数的地址传到AX,然后"PUSH AX"把参数的地址压入堆栈.最后"CALL 函数地址"转向执行要调用的函数.最后调用完后,"POP CX",恢复堆栈指针SP.

研究案例二

工具: Turboc C v2.0,Debug,MASM v5.0,NASM,TASM
实例C程序:
/* example1.c */
char ch;

extern void e_putchar(char c);
int e_main()
{

ch=0x44;
e_putchar(ch);

}
实例汇编程序:
; eio.asm
_TEXT segment byte public 'CODE'
DGROUP group _TEXT
   assume cs:_TEXT,ds:DGROUP,ss:DGROUP
  public _k_putchar
_k_putchar proc near
push   bp
mov   bp,sp
mov   ah,0eh
mov   bx,7h
mov   al,byte ptr [bp+4]
int   10h
pop   bp
ret
_k_putchar 
endp

目标内容:C语言中函数使用参数的方法

这一节我们将使用TASM用汇编来写个标准的C函数.这一节的内容大家可能在很多汇编的书籍上都看到过.讲的是C语言和汇编语言的连接方法.可能你会奇怪,我们这里已经有了MASM,NASM两个汇编编译器了,为什么还要使用TASM另外一个汇编编译器.我不知道MASM是否可以和我们的Turboc C配合,但是TASM是肯定可以和Turboc C完全配合的.毕竟它们都是Borland公司的产品,而且Turboc C中用-S生成的汇编代码是完全按照TASM中的语法而定的.这足以见Turboc C和TASM之间"亲密的"关系了.

这个案例中我们主要并不研究C代码了.而是研究那个用汇编写的C函数.
push   bp
mov   bp,sp
mov   ah,0eh
mov   bx,7h
mov   al,byte ptr [bp+4]
int   10h
pop   bp
ret
其中byte ptr[bp+4]就是我们传给e_putchar()的参数值.
前一个案例中我们一直知道了C语言是把参数的地址压入堆栈的方式传给函数.所以在标准的C函数中,都是通过取堆栈里的值来读参数.
标准的C函数前两行都是
push   bp
mov   bp,sp

首先保存BP的值,然后把当前的堆栈指针传递给BP,我们访问传递给该函数的参数就是通过BP.而第一个参数值就放在BP+4的地址中,第二个参数值就放在BP+6,...,这样依此对应每个参数的地址.BP就是CALL调用前的IP的值.因为CALL执行的时候,系统会自动把当前的IP压入堆栈.关于这个前面一个案例中已经给出介绍.

别看这个C函数是用汇编语言写的,它可是个完完整整的C函数.
好了,让我们编译出来看看.

TASM eio.asm eio.obj
TCC -mt -oexample1.obj -c example1.c
link start.obj eio.obj example1.obj,example1.exe,,,
exe2bin example1.exe


好了,该是我们总结的时候了.
C语言中函数访问参数的方法就是先通过"PUSH BP"保存BP,"MOV BP,SP"把当前的堆栈指针传递给BP.第一个参数的地址就在BP+4,第二个参数的地址就在BP+6,...比如"MOV AX,WORD PTR[BP+4]"就可以把第一个参数值传给AX 寄存器.而需要留意的是C/C++传递参数的顺序是和其它语言相反的.C语言是把参数的地址从右到左压入堆栈,所以越后面的参数,在堆栈中的地址越靠前.


研究案例三

工具: Turboc C v2.0,Debug,MASM v5.0,NASM
实例C程序:
/* example3.c */
char ch;

int e_main()
{
int i1;
int i2;
int i3;
i1=1;
i2=2;
i3=3;

}

; C程序的入口 start.asm
[BITS 16]
[global start]
[extern _e_main]
start:
   call _e_main

目标内容:C语言使用局部变量的方法

同样,这里我需要使用start.asm来作为我们C语言的入口.我们使用e_main,避开常规main函数入口,这样我们就能更清晰地了解到函数内部所产生的代码指令.

跟前一节一样,我们先编译C程序和入口汇编程序start.asm
nasmw -f obj -o start.obj start.asm
TCC -mt -oexample3.obj -c example3.c
link start.obj example3.obj,example3.exe,,,
exe2bin example3.exe

同样,我们使用老DOS的DEBUG工具来对example3.bin进行反汇编查看C生成的代码.

DEBUG
-n example3.bin
-l 0
-u 0
xxxx:0000     CALL   0003
xxxx:0003     PUSH    BP
xxxx:0004     MOV     BP,SP
xxxx:0006     SUB     SP,+06
xxxx:0009     MOV    WORD PTR [BP-06],
0001
xxxx:000E     MOV    WORD PTR [BP-04],
0002
xxxx:0014     MOV    WORD PTR [BP-02],
0003
xxxx:0019     MOV     SP,BP
xxxx:001B     POP     BP
xxxx:001C     RET

好了,这里关于C生成的代码已经显露出来了.除开第一句CALL 0003是我们在start.asm的代码外,其它就是我C程序生成的代码.
首先进入e_main函数.执行

PUSH BP
MOV   BP,SP

这跟我们前面第二个案例中函数反问参数的代码相同.先保存BP,然后把堆栈指针传递给BP,以便后面通过BP来实现对变量的访问.

SUB   SP,+06
将堆栈指针继续后移动6个字节.因为我们在e_main中定义三个整型变量i1,i2,i3,一共6个字节的空间.这里通过移动堆栈指针,来实现局部变量的内存空间分配.

MOV  WORD PTR [BP-06],0001
MOV  WORD PTR [BP-04],0002
MOV  WORD PTR [BP-02],0003
分别对应我们在e_main中的三条赋值语句
i1=1;
i2=2;
i3=3;
这里我们可以看出,i1的地址实际上就是BP-06,i2就是BP-04,i3就是BP-02.前面的SUB SP,+06就是为了这三个变量而留出6个字节的空间(BP-08到BP-02)
同时我们也看到C语言中16位的赋值语句就是简单的MOV指令完成的.

MOV   SP,BP
当e_main函数结束后,堆栈指针还原成BP(BP值从未改变过).这样,我们的局部变量i1,i2,i3的空间也就消失了.所以当C语言中的函数结束后,函数中的局部变量会自动消失.

POP   BP
还原BP的值.这与前面的PUSH BP想对应

好了,本案例研究完毕.下面是总结的时候了.
C语言函数中的局部变量的空间一般都是放在堆栈里面.在进入函数前,通过"SUB SP,+XX"来为这些局部变量分配堆栈空间.然后同样通过BP来对这些局部变量进行访问.函数结束时,"MOV SP,BP"还原堆栈指针,局部变量随之而消失.最后以"POP BP"还原BP,结束该函数.

值得注意的是,C语言会自动为C函数中经常使用int类型变量设置成resigter int.这样的局部变量就不是使用堆栈空间的了,而就是直接使用SI寄存器.
比如一个典型的例子
void loop()
{
int i;
while(i<10000)
{
   i++;
}
}
对于这样的函数,C语言通常会将i优化成resigter int i.这个i没有使用任何内存空间来保存数值,它的数值直接保存于SI寄存器.那么对它的访问速度自然比起一般的变量要快.





你可能感兴趣的:(tony --- C语言那些事儿(1))