我为人人,人人为我!帮助    偶元赚取方式
发新话题
打印

[原创]重新学习C语言 程序员的基本功(不仅仅是c语言哦)

本主题由 admin 于 2008-5-15 17:50 移动
4 c库
C语言的特性相对比较少,其他类c语言也是一样.OO的语言中的class可以看做一个特殊的struct,对象的方法其实就是函数(只是要求有一个默认的指向其对象的地址作为参数,比如this).用c现有的元素也可以模拟许多OO的特性,比如interface,多态等等.我见过一些纯c程序,也使用了后绑定,interface等特性降低程序的耦合以及其他优良的特性.

知道c语言的语言特性,就可以编程了吗?当然可以,但仅仅是语言本身除了学习之外,好像还没有别的什么用途!

那么,真正的软件开发需要哪些知识呢?
先扯点题外话
( 看上去好像与软件开发无关,其实却是软件开发成功决定性的一个概念: “软件工程”
人类似乎总是遭到天神的嘲弄,比如给了人类了解”天意”的智慧,却没有给予足够的时间,人类连自己也搞不清楚,却要去”改造大自然”.软件行业也是一个典型,现代自动化和高科技支撑产业的软件行业,却是几千年前就存在的”手工业作坊”,知识工具变成了键盘,鼠标.
  一个软件的成功有许多环境,包括可行性研究,需求分析,设计,开发,测试,发布,维护等等,软件的开发只是其中一部分,随着需求的复杂度的提升,”开发”这个”过程”对于软件工程成功与否的重要性在逐渐降低!
  )
好了先不管其他东西,只是软件开发,在我们熟悉了一门语言之后还需要了解什么呢?
当然不同的软件工程需要不同的知识体系,但计算机的基本知识体系大概是差不多的!
无论是服务器软件,还是桌面应用,或者是嵌入式开发都离不开计算机本身,记得我的计算机启蒙老师说过一句话(1996年,当时win95盛行,我们学习还是在Novell环境和dos下学习汇编)”别说是win95,就是将来有了win2000(后来真的有了)也是在计算机上运行的,不是天上飘的”.潜台词是让我们充分了解计算机原理.
好了,接下来我要讲开发软件可能会涉及到的几个重要概念,许多与计算机(c语言的宿主)打交道的东西,c语言通过库的形式提供.另外,我选择Linux和GCC环境,或许这是学习c语言的最好的环境了(虽然我是个windows程序员).
   第一个概念,库本身是通过什么载体提供的?计算机中可执行的程序是什么样子?
这里面涉及到可执行文件的格式,静态链接和动态链接的概念,(进程的概念在并发中提及)
当然每一个概念都涉及到很多内容,我只是做简单的讲述,需要深入的地方,另外大部分概念都是最终由操作系统提供服务的,OS的概念太大,庐山太大,还是先认识花草吧!
   第二个重要的概念是c语言中似乎缺少的语言特性,程序级异常,当然c是通过库函数提供的,功能可以用,还在乎什么是否是”语言特性”.
   第三个也是语言相关的,关于动态内存分配.在c++/Delphi等OO语言,动态内存分配扮演着重要的角色,OO中的”对象”一般是用户掌握生存期的,而动态内存分配符合这个特性.
   第四个是windows下最得意的部分,关于UI,用户接口(用户界面),当然用户界面一直是以Unix的命令行界面和Mac的GUI界面两个方向,本来没有什么技术的优劣,只是不同的资源和用途决定的,你不觉的地铁门上面只有一行的LED指示牌有什么不好吧!
   第五个就是程序运行和并发!进程,线程,还有其他的吗?并发和内存的动态分配一直是大多数程序员困扰的问题.为什么呢?
   第六个概念是几乎所有计算机书上都讲的IO!  File的概念或许和进程的概念一样,是计算机领域最重要的概念之一.在Unix/Linux世界里,大多数设备都是靠”打开,读,写,控制,关闭”这五个概念操作的,能用这五个概念操作的都可以叫IO!键盘,显示器,磁盘文件(光盘什么的也一样),串口,并口,红外,USB,网络.Windows里其实也一样!
   第七个概念是IO的一部分(或叫做延伸,internet可以算作是你硬盘的延伸):网络.未来的软件,应该总是围绕网络的.不是在提供服务,就是在请求服务,或者兼而有之!
   第八个概念属于应用范畴了:数据库.做应用软件似乎离不开数据库的概念,数据库是一个存储,传输,数据分析等等综合的概念.
   
   当然,或许还有许多重要的概念! 不同领域的软件开发遇到的问题差异很大,但上面的概念或多或少要涉及到!我也是在不断的学习领悟,欢迎拍砖!

TOP

现在进入话题! 等等先说点题外话!
大三的时候我突然喜欢上计算机,一个偶然的机缘,我参加了一个计算机原理(汇编语言)的学习班(70学时,每学时3块钱,这么贵的培训,可见我是下了很大的勇气的).掌握了一些微机的基本知识,某日在机房看到几个同学用debug (dos上最有用的工具了)调试一段报纸上抄的程序,功能是用程序实现重启计算机,搞了很久,我过去看了看,只写了一行代码 jmp FFFF:0
然后w存盘,大概只有5个字节的.com执行文件,运行后那台386重启了,当时我那种神气的表情恐怕日后很难再有了!
我讲这个故事是为了说明:理解原理更重要! 就现在来说,能够轻松在百度,google搜索到的所谓技巧等等只能算是”九阴白骨爪”,用得到的时候再查也不迟,那些需要你不断实践从中感悟到的东西才是真正的”九阳真经”.

下面的内容如果在实践中碰到问题需要了解,大家可以深入研究相关的领域,没必要先练好所有武功再出道,有了一定的基础,然后不断的实践再学习效果会更好!如果某些知识今后的实践中用不到,就等用的时候再了解也不迟!但一些原理性的东西还是需要掌握,需要的时候就知道从哪个方向入手,否则永远是一头雾水!

(一)        可执行文件可链接文件.
(涉及到编译原理,操作系统,动态链接等知识)
(1)        c程序的编译过程.
一个c程序的文本文件(ASCII码文件)如何变成一个可执行的程序? C程序大概需要这么几步:
1预处理
2编译
3汇编
4链接

预处理是将c文件中的根据#开始的命令修改生成新的源程序,比如宏替换,#include将相应的文件内容包含进来.
编译就是把预处理后的c源文件转换成汇编语言源文件(仍然是文本文件).对于特定的宿主(操作系统和CPU体系),汇编语言是一样的,c和pascal编译后得到一样的汇编语言.所以这样类型的语言都可以叫做编译语言
汇编 是把汇编语言文件翻译为机器指令(二进制文件).这种格式一般叫可重定位目标程序
就是大家熟悉的obj文件,(下面的文件格式再说)
链接 链接的结果是一个可执行文件(一般指由操作系统直接加载的文件).程序中用到的外部变量和函数的定义是在链接过程中实际定位的!编译汇编时只需要有声明就可以了!(相关的知识需要了解链接时的符号解析等等,有兴趣的可以看看).

例子说明!
  打开vi 编辑个strlen.c的文件
#define STREOF ‘\0’
  int strlen_lf(char *str)
{
  int len=0;
  for(len=0; *str != STREOF; str++)
    len++;
  return len;
}
保存,用gcc编译看看
gcc -c strlen.c -save-temps
(  gcc 的基本用法
  -save-temps  表示保留中间文件,一般情况
  strlen.i 预处理后的c源文件
  strlen.s 汇编语言文本文件
  strlen.o obj文件
  单独的参数为:
  -E 只做预处理
  -S 只做到编译
  -c 只做到汇编
  默认是一直做到链接
)
为了说明情况,我又写了一个strtest.h 文件用来声明
strtest.h---------------------
#define STREOF '\0'
int strlen_lf(char *str);
void strcpy_lf(char *dest ,const char *src);

strlen.c--------------------------
#include "strtest.h"

int strlen_lf(char *str)
{
  int len=0;
        for(len=0; *str != STREOF; str++)
          len++;
        return len;
}
编译
gcc -c strlen.c -save-temps
然后看看预处理后的文件strlen.i
# 1 "strlen.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "strlen.c"
# 1 "strtest.h" 1


int strlen_lf(char *str);
void strcpy_lf(char *dest ,const char *src);
# 2 "strlen.c" 2

int strlen_lf(char *str)
{
  int len=0;
for(len=0; *str != '\0'; str++)
   len++;
return len;
}
#include “strtest.h” 被展开到文件, STREOF 被替换为 ‘\0’

有兴趣的可以看看strlen.s  一个标准的GAS文件(GNU 汇编程序).

现在写个执行程序调用一下strlen-lf 顺便看看c声明和定义在编译时期的表现!
  再加入一个externvalue.c 文件 就一行
int externvalue=10;

下面是main1.c
#include "strtest.h"
#define HELLO "Hello,world"
extern int externvalue;
int main(int argc, char *argv[])
{
  int len = strlen_lf(HELLO);
  //printf("%s  length is %d\n",HELLO,len);
  return externvalue;
}

gcc –c main1.c –save-temps
没有执行链接!
看看main1.c
  .file "main1.c"
  .section  .rodata
.LC0:
  .string "Hello,world"
  .text
.globl main
  .type main, @function
main:
  pushl %ebp
  movl  %esp, %ebp
  subl  $8, %esp
  andl  $-16, %esp
  movl  $0, %eax
  addl  $15, %eax
  addl  $15, %eax
  shrl  $4, %eax
  sall  $4, %eax
  subl  %eax, %esp
  subl  $12, %esp
  pushl $.LC0
  call  strlen_lf
  addl  $16, %esp
  movl  %eax, -4(%ebp)
  movl  externvalue, %eax
  leave
  ret
  .size main, .-main
  .section  .note.GNU-stack,"",@progbits
  .ident  "GCC: (GNU) 3.4.6 20060404 (Red Hat 3.4.6-9)"



externvalue 和 strlen_lf 只是一个符号而已!
编译strlen.c  externvalue.c
gcc –c strlen.c externvalue.c
看看生成的汇编程序,循环在汇编里就是比较和跳转实现的.

TOP

读者太少! 写下去的动力比较小啊!

TOP

好贴, 起来, 支持楼主, 期待续文。     

TOP

(2)        可执行文件的格式和内存映像
在windows平台上可执行程序就是exe文件,dos时代的小于64K的com文件现在已经不见了!windows上可执行文件格式我们一般成为PE(Portable Executable)文件(.net 编译成的exe也是PE文件,但有本质的差别),PE文件源于Unix系统的COFF文件.现代Unix系统,比如Linux使用一种叫ELF(Executable and Linkable Format)的文件,意思是可执行和可链接的文件,名称概况的很好! 可执行文件,可重定位的目标文件(obj)以及动态链接库文件(.so)文件都是ELF文件. Windows上的动态库是DLL文件!
  Windows的PE格式也比较简单,大概由4个部分组成 文件头,节表,节以及其他如调试信息等部分.文件头至今还保留了Dos文件头(dos时代比较熟悉的4D5A(“MZ”)开头),以及输出一句提示语”This program must be run under Wind32”.
其实PE格式和ELF格式都有许多相同的地方,包括可执行文件加载到内存中的内存映像(就是进程,进程、文件和虚拟储存器是操作系统的几个重要的抽象概念,当然windows是以线程调度的,这在后面的内容讨论)也是大同小异!所以就简单的说说linux中的ELF。详细深入的研究可以参考各种关于ELF文件的文章(google,百度去吧)。
  可执行文件,共享库文件(动态链接库)以及obj文件的ELF格式都有区别,但都有ELF头,节表,节的部分组成。有一条需要记住,可执行程序文件是由编译器生成的,所以具体语言如何实现(还有编译器优化的影响)是编译器决定的.下面简单的说说几个重要的节:
.init  可执行程序初始化部分,只读的  
.text    已编译的机器代码 (windows上一般成为代码段),只读的.
.rodata  保存一些字符串字面量等等的只读数据段,只读的.
.data  c中已经初始化的全局变量(就是数据段)
.bss  c中未初始化的全局变量,不实际占有空间,只有占位符,目的是为了节省空间;
.symtab 符号表 存放引用外部的全局变量和函数信息.链接时候需要符号解析,编译汇编时有符号(变量和函数)的声明就可以了,这样简单的设计也给链接带来很多麻烦(后来许多语言有了命名空间的概念),c提供了static 修饰外部变量和函数,不需要文件外访问的外部变量和函数就用static修饰可以解决许多问题..
如果是可链接的目标文件(obj .o文件) 还有 .rel.text 和.rel.data 节,一般是在链接时需要修改的(重新定位地址信息).
如果是-g参数编译,还有调试信息
.debug 调试符号表
.line 是c代码和.text机器代码的映射(调式器为何能定位到代码行?哈哈)

符号表根据汇编的符号生成,包括模块内部符号(static修饰的),外部引用(函数和extern 的外部变量)以及模块内部定义被其他模块引用的全局符号.符号表的解析在链接过程中处理.解析的原则也很简单.需要了解的可以找文章或书进一步研究(对c语言编写有很大帮助哦).Unix/linux的设计总是定义一些简单的原则,给程序员很大的发挥空间(这难道是一种哲学?)
   可执行程序由操作系统加载到内存中(不是可执行程序的所有内容都加载,这涉及操作系统虚拟储存器的概念,另外调试信息等也不一定加载到内存中).linux可执行程序的内存映像大概是这样的(Win32也有许多相似的地方,可以参看windows内核方面的书籍,个版本windows的进程映像不太相同)
  <图片> Linux可执行文件的内存映像
认识上面的这幅图非常重要!
在Linux系统上,代码段总是从0x08048000处开始.上面是用户堆,通过调用malloc分配(其实是Linux的brk系统调用分配,内存动态分配后面会讨论).0x40000000处是调用的共享库映射.
栈底是从0xbfffffff处开始(栈内存地址小),0xc0000000以上是内核使用(与win32上1G内核内存模式类似).(我在十年前dos编程时遇到的代码段,数据段等内存映射知识到现在似乎变化不是很大,还有写汇编用的CPU寄存器,只是16位的AX,现在是32位的EAX,变化也不大,不是说计算机知识发展是一日千里吗?)

[ 本帖最后由 hotplum 于 2008-2-20 22:36 编辑 ]
附件: 您所在的用户组无法下载或查看附件

TOP

引用:
原帖由 ahlzl 于 2008/2/13 11:11 发表
现在,我很难静下心去学C语言呀!
同感啊!不过在此还是要谢谢楼主的分享!!
坚持到底!!

TOP

(3)        静态链接和动态链接.
关于可执行文件格式,有许多好于的工具可以使用:window平台上查看PE格式的工具很多(比如PEJump等,百度google去吧),Linux上有个很好的工具是objdump,自己看帮助文件objdump还可以反汇编哦.
静态链接比较简单,其实就是将obj文件打包一下.先做个试验看看吧!
就使用上面用到的main1.c strtest.h strlen.c externvalue.c ,我们再家一个文件strcpy.c
#include "strtest.h"
void strcpy_lf(char *dest,const char *src)
{
  while((*dest++ = *src++) != STREOF)
         ;
}
(我使用了stcpy_lf 这样的名字是区别于标准c库的,因为在gcc编译是默认链接标准库的,
会有名字冲突,你可以用-static 参数编译main2.c看看链接后的可执行文件大小)
main2.c
#include <stdio.h>
#include "strtest.h"
#define HELLO "Hello,world"
int main(int argc, char *argv[])
{

        char str[16];
        strcpy_lf(str,HELLO);
        int len = strlen_lf(str);

        printf("%s  length is %d\n",str,len);
        return 0;
}
(这回使用了标准输出的printf函数,检测一下上面两个函数是否正确)
编译三个c文件
gcc –c strlen.c strcpy.c externvalue.c
  然后试试 gcc -o main2 main2.c
大概是下面的链接器错误信息,找不到引用的符号.
  /tmp/ccUE4aQH.o(.text+0x29): In function `main':
: undefined reference to `strcpy_lf'
/tmp/ccUE4aQH.o(.text+0x38): In function `main':
: undefined reference to `strlen_lf'
/tmp/ccUE4aQH.o(.text+0x5e): In function `main':
: undefined reference to `externvalue'
collect2: ld returned 1 exit status

gcc -o main2 main2.c strlen.o strcpy.o externvalue.o
这回OK了!
运行结果如下:
./main2
Hello,world  length is 11
externvalue =10

把上面的三个.o(obj)文件打包成静态库
ar rcs libstrtest.a strlen.o strcpy.o externvalue.o
gcc -c main2.c
使用静态链接库
gcc -o main2 main2.o ./libstrtest.a

对于静态链接,main2程序中用到的上面三个obj文件都被链接到main2程序中,成为一个整体.比如标准c库,系统中会有许多程序用到,也就是说有许多份标准库程序的拷贝,尤其是加载到内存中,会浪费大量的空间!
  动态链接解决了这个问题!
  动态链接库对于win32程序员来说太熟悉了.在Win32中操作系统的API(Linux下叫系统调用)都是由动态库承载的,包括一些系统资源文件也是动态库.动态库也是com组件的载体(dll,ocx),即使.net推广了N年后的今天,Win32的大多数核心功能还是dll和com提供的!未来会怎样?在可以预见的未来,动态库这种技术还会一直存在和发展.因为win32下的动态库是OS提供的统一标准,各种编程语言之间可以通过动态库互相访问(当然要统一函数调用规则和数据类型,com组件其实就是定义了一致的调用规则和元数据),因为动态库是动态链接加载的,所以只要输出的接口不变,可以修改动态库中实现,为只做比较大型的软件提供了封装模块的方法,而且动态库在OS中只被加载一次,可以多个执行程序共享,节省了空间.另外因为动态库中的数据和函数被映射的主程序的进程空间,可以很方便的互相访问(进程外com技术实现两个进程共享数据其实也是通过动态库作为桥梁实现的).不说了,总之win32下使用动态库似乎是必须掌握的技术.好在编写和使用都比较简单.
linux下的动态库和win32的机理大同小异,相比之下linux编写动态库更为简单,不用显示的export函数,也不需要DllMain. 在Linux下动态链接也是非常重要的,c标准库的提供.gcc默认链接标准c库的.可以试试看
编写testlibc.c

int main()
{
    return 0;
}
就这样简单
gcc testlibc.c -o testlibc
然后用ldd看看testlibc需要的so ,下面是我电脑上的查看结果
ldd testlibc
        libc.so.6 => /lib/tls/libc.so.6 (0x002a9000)
        /lib/ld-linux.so.2 (0x0028f000)
libc.so.6 就是标准c库的动态库
ld-linux.so.2 是加载其他动态链接库的动态链接器(他本身也是个动态链接库在ELF的.interp节中指明)
可以用下面的命令看看
objdump -s testlibc |more

testlibc:     file format elf32-i386

Contents of section .interp:
8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
8048124 2e3200                               .2.


好吧 先做个实验吧!
我们把上面的用到的几个c文件编译为动态链接库!
gcc -shared -fPIC -save-temps -o libstrtest.so externvalue.c strlen.c strcpy.c
gcc使用shared参数给链接器
-fPIC 要求生成与位置无关的代码(有兴趣的可以进一步研究,主要是因为动态库加载需要指定相应的内存映像地址信息,与位置无关则可以在任何地址加载)
然后编译main2.c
gcc –o main3 main2.c ./libstrtest.so

main3 和main2 两个文件运行结果相同
不同的是main3 运行需要libstrtest.so
ldd main3 看看!

这样的调用动态库属于隐式调用(win32上也一样)!还有一种是显式调用,就是在程序中通过库函数调用指定的动态库.在win32上使用下面三个API函数
LoadLibrary 装载动态库。
GetProcAddress 获取要引入的函数,将符号名或标识号转换为DLL内部地址。
FreeLibrary 释放动态链接库。

相对应的linux的函数为
dlopen
dlsym
dlclose
另外提供了dlerror 得到上面三个函数出错信息.
下面使用上面的libstrtest.so 修改main2.c 为main4.c
#include <stdio.h>
#include <dlfcn.h>  //操作动态库的头文件
#define HELLO "Hello,world"
int main(int argc, char *argv[])
{
  int *pExternvalue;
  int (*strlen_lf)(char *);
  void (*strcpy_lf)(char *, const char *);
  void *handle;
  char *error;
  handle = dlopen("./libstrtest.so",RTLD_LAZY);
  if (!handle)
  {
    fprintf(stderr,"%s\n",dlerror());
    return 1;
  }
  strlen_lf = dlsym(handle,"strlen_lf");
  if((error = dlerror()) != NULL)
  {
    fprintf(stderr,"%s\n",error);
    return 1;
  }
  strcpy_lf = dlsym(handle,"strcpy_lf");
  if((error = dlerror()) != NULL)
  {
    fprintf(stderr,"%s\n",error);
    return 1;
  }
  pExternvalue = dlsym(handle,"externvalue");
  if((error = dlerror()) != NULL)
  {
    fprintf(stderr,"%s\n",error);
    return 1;
  }

  char str[16];
  strcpy_lf(str,HELLO);
  int len = strlen_lf(str);
  printf("%s  length is %d\n",str,len);
  printf("externvalue =%d\n",*pExternvalue);
  if (dlclose(handle)<0)
  {
    fprintf(stderr,"%s\n",dlerror());
    return 1;
  }
  return 0;
}

编译
gcc -o main4 main4.c –ldl
-ldl 表示链接dl的库
看上去非常简单!
显式调用 (动态调用)动态库非常有用! 在比较大型的软件设计中,可以根据用户配置用动态库实现的各种模块,缺少某个或某几个动态库依然能运行系统,只是缺少相应的功能模块而已!
另外如果在自己实现的脚本语言中调用动态库就有一定的难度(需要借助于汇编实现,参数返回值等的运行时传递,需要比较底层的技术),相对来说指定了调用规则和数据格式的com就比较容易(例如,微软的JS就可以有条件的调用com组件).
在linux上比较大的应用软件也是借助动态链接库实现功能模块的灵魂配置和加载.

TOP

引用:
原帖由 liufeng55255 于 2008-2-21 12:47 发表



同感啊!不过在此还是要谢谢楼主的分享!!
其实重在我题目的后半部分,"程序员的基本功"!

面对国内软件人才匮乏,国内软件公司技术含量低,归根结底是我们浮躁的软件文化...

TOP

(3)        静态链接和动态链接.
关于可执行文件格式,有许多好于的工具可以使用:window平台上查看PE格式的工具很多(比如PEJump等,百度google去吧),Linux上有个很好的工具是objdump,自己看帮助文件objdump还可以反汇编哦.
静态链接比较简单,其实就是将obj文件打包一下.先做个试验看看吧!
就使用上面用到的main1.c strtest.h strlen.c externvalue.c ,我们再家一个文件strcpy.c
#include "strtest.h"
void strcpy_lf(char *dest,const char *src)
{
  while((*dest++ = *src++) != STREOF)
         ;
}
(我使用了stcpy_lf 这样的名字是区别于标准c库的,因为在gcc编译是默认链接标准库的,
会有名字冲突,你可以用-static 参数编译main2.c看看链接后的可执行文件大小)
main2.c
#include <stdio.h>
#include "strtest.h"
#define HELLO "Hello,world"
int main(int argc, char *argv[])
{

        char str[16];
        strcpy_lf(str,HELLO);
        int len = strlen_lf(str);

        printf("%s  length is %d\n",str,len);
        return 0;
}
(这回使用了标准输出的printf函数,检测一下上面两个函数是否正确)
编译三个c文件
gcc –c strlen.c strcpy.c externvalue.c
  然后试试 gcc -o main2 main2.c
大概是下面的链接器错误信息,找不到引用的符号.
  /tmp/ccUE4aQH.o(.text+0x29): In function `main':
: undefined reference to `strcpy_lf'
/tmp/ccUE4aQH.o(.text+0x38): In function `main':
: undefined reference to `strlen_lf'
/tmp/ccUE4aQH.o(.text+0x5e): In function `main':
: undefined reference to `externvalue'
collect2: ld returned 1 exit status

gcc -o main2 main2.c strlen.o strcpy.o externvalue.o
这回OK了!
运行结果如下:
./main2
Hello,world  length is 11
externvalue =10

把上面的三个.o(obj)文件打包成静态库
ar rcs libstrtest.a strlen.o strcpy.o externvalue.o
gcc -c main2.c
使用静态链接库
gcc -o main2 main2.o ./libstrtest.a

对于静态链接,main2程序中用到的上面三个obj文件都被链接到main2程序中,成为一个整体.比如标准c库,系统中会有许多程序用到,也就是说有许多份标准库程序的拷贝,尤其是加载到内存中,会浪费大量的空间!
  动态链接解决了这个问题!
  动态链接库对于win32程序员来说太熟悉了.在Win32中操作系统的API(Linux下叫系统调用)都是由动态库承载的,包括一些系统资源文件也是动态库.动态库也是com组件的载体(dll,ocx),即使.net推广了N年后的今天,Win32的大多数核心功能还是dll和com提供的!未来会怎样?在可以预见的未来,动态库这种技术还会一直存在和发展.因为win32下的动态库是OS提供的统一标准,各种编程语言之间可以通过动态库互相访问(当然要统一函数调用规则和数据类型,com组件其实就是定义了一致的调用规则和元数据),因为动态库是动态链接加载的,所以只要输出的接口不变,可以修改动态库中实现,为只做比较大型的软件提供了封装模块的方法,而且动态库在OS中只被加载一次,可以多个执行程序共享,节省了空间.另外因为动态库中的数据和函数被映射的主程序的进程空间,可以很方便的互相访问(进程外com技术实现两个进程共享数据其实也是通过动态库作为桥梁实现的).不说了,总之win32下使用动态库似乎是必须掌握的技术.好在编写和使用都比较简单.
linux下的动态库和win32的机理大同小异,相比之下linux编写动态库更为简单,不用显示的export函数,也不需要DllMain. 在Linux下动态链接也是非常重要的,c标准库的提供.gcc默认链接标准c库的.可以试试看
编写testlibc.c

int main()
{
    return 0;
}
就这样简单
gcc testlibc.c -o testlibc
然后用ldd看看testlibc需要的so ,下面是我电脑上的查看结果
ldd testlibc
        libc.so.6 => /lib/tls/libc.so.6 (0x002a9000)
        /lib/ld-linux.so.2 (0x0028f000)
libc.so.6 就是标准c库的动态库
ld-linux.so.2 是加载其他动态链接库的动态链接器(他本身也是个动态链接库在ELF的.interp节中指明)
可以用下面的命令看看
objdump -s testlibc |more

testlibc:     file format elf32-i386

Contents of section .interp:
8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
8048124 2e3200                               .2.


好吧 先做个实验吧!
我们把上面的用到的几个c文件编译为动态链接库!
gcc -shared -fPIC -save-temps -o libstrtest.so externvalue.c strlen.c strcpy.c
gcc使用shared参数给链接器
-fPIC 要求生成与位置无关的代码(有兴趣的可以进一步研究,主要是因为动态库加载需要指定相应的内存映像地址信息,与位置无关则可以在任何地址加载)
然后编译main2.c
gcc –o main3 main2.c ./libstrtest.so

main3 和main2 两个文件运行结果相同
不同的是main3 运行需要libstrtest.so
ldd main3 看看!

这样的调用动态库属于隐式调用(win32上也一样)!还有一种是显式调用,就是在程序中通过库函数调用指定的动态库.在win32上使用下面三个API函数
LoadLibrary 装载动态库。
GetProcAddress 获取要引入的函数,将符号名或标识号转换为DLL内部地址。
FreeLibrary 释放动态链接库。

相对应的linux的函数为
dlopen
dlsym
dlclose
另外提供了dlerror 得到上面三个函数出错信息.
下面使用上面的libstrtest.so 修改main2.c 为main4.c
#include <stdio.h>
#include <dlfcn.h>  //操作动态库的头文件
#define HELLO "Hello,world"
int main(int argc, char *argv[])
{
  int *pExternvalue;
  int (*strlen_lf)(char *);
  void (*strcpy_lf)(char *, const char *);
  void *handle;
  char *error;
  handle = dlopen("./libstrtest.so",RTLD_LAZY);
  if (!handle)
  {
    fprintf(stderr,"%s\n",dlerror());
    return 1;
  }
  strlen_lf = dlsym(handle,"strlen_lf");
  if((error = dlerror()) != NULL)
  {
    fprintf(stderr,"%s\n",error);
    return 1;
  }
  strcpy_lf = dlsym(handle,"strcpy_lf");
  if((error = dlerror()) != NULL)
  {
    fprintf(stderr,"%s\n",error);
    return 1;
  }
  pExternvalue = dlsym(handle,"externvalue");
  if((error = dlerror()) != NULL)
  {
    fprintf(stderr,"%s\n",error);
    return 1;
  }

  char str[16];
  strcpy_lf(str,HELLO);
  int len = strlen_lf(str);
  printf("%s  length is %d\n",str,len);
  printf("externvalue =%d\n",*pExternvalue);
  if (dlclose(handle)<0)
  {
    fprintf(stderr,"%s\n",dlerror());
    return 1;
  }
  return 0;
}

编译
gcc -o main4 main4.c –ldl
-ldl 表示链接dl的库
看上去非常简单!
显式调用 (动态调用)动态库非常有用! 在比较大型的软件设计中,可以根据用户配置用动态库实现的各种模块,缺少某个或某几个动态库依然能运行系统,只是缺少相应的功能模块而已!
另外如果在自己实现的脚本语言中调用动态库就有一定的难度(需要借助于汇编实现,参数返回值等的运行时传递,需要比较底层的技术),相对来说指定了调用规则和数据格式的com就比较容易(例如,微软的JS就可以有条件的调用com组件).
在linux上比较大的应用软件也是借助动态链接库实现功能模块的灵魂配置和加载.

TOP

(二)        程序级异常
对于一种现代编程语言来说,对异常控制流的支持是非常重要的!尤其是开发过类库的同学一定深有体会! 举个例子,比如你开发一个读取某种自定义XML格式的类库(比如叫做LFML吧,用来描述一种特定业务),当用户程序使用你的类库时有一种情况是肯定的,就是不管你的LFML描述的格式多么复杂(肯能需要许多函数去解析,还有函数的嵌套调用),但如果格式被破坏,只要有一条不符合你的LFML规则,你的解析就不需要进行下去了!这样的程序如何实现?如果有异常控制流,只需要在任意函数(不管嵌套多少层)中抛出一个异常,在客户程序中捕获这个异常就可以了!
当然程序级的ECF(异常控制流)还有许多有用的地方!
在操作系统中,举个例子,你的程序正在执行,这是用户按下一个ESC键,操作系统如何告知你ESC键按下(或者正在运行的程序如何知道ESC键按下),在dos时代有个常见的词-中断.
其实中断也是一种ECF!还有你的程序在调用内核的功能时(因为你的程序运行在用户空间),内核运行在内核空间,如何完成这种调用呢?还有,操作系统是如何完成多个进程(线程)”同时”执行的呢?这些都是OS级的ECF!需要这方面的资料可以baidu,google!
  我这里只讨论程序级的ECF!
在程序中嵌套函数的调用,需要有比较复杂的调用栈(任何语言都是有的),如果从一个深层的嵌套函数中返回,需要解开这些复杂的调用栈!程序级异常事先保留了需要恢复的栈内容,所以允许从一个深层嵌套的函数调用中立即返回!
下面看一个JS的例子(c++,java等语言也大同小异)

var exception= {};
exception.msgno=0;
exception.msg='';

main();

function main()
{
  try
  {
    throwexcept(1)
  }
  catch(pException)
  {
    writeln("msgno="+pException.msgno+",msg="+pException.msg);
  }
}

function throwexcept(errno)
{
  if(errno == 0)
  {
    exception.msgno=100;
    exception.msg="I'am hungry!";
    throw exception;
  }
  otherexcept();
}

function otherexcept()
{
  exception.msgno=200;
  exception.msg="I need lover!";
  throw exception;
}


了解js的同学可以试验一下! 可以下载我写的JS工具测试运行!
http://www.51leifeng.net/thread-6259-1-1.html

js中try catch 用来设置恢复点,捕获异常, throw 用来抛出一个异常(js中是一个对象,c++中其实就是一个指针,区别不大)
(异常和错误是两个概念!请自己理解 ,如果没有任何地方throw,就永远不会catch到)

在c语言中用setjmp 和longjmp 库函数实现用户级的ECF!

把上面JS的例子改写一下:
#include <setjmp.h>


struct Exception
{
  int msgno;
  char msg[256];
}exception;

jmp_buf buf;

void throwexcept(int);
void otherexcept(void);
int main(int argc,char *argv[])
{
  struct Exception *pException;
  long rc;
  rc = setjmp(buf);
  if(rc == 0)
  {
    throwexcept(0);
  }else
  {
    pException =(void *)rc;
    printf("msgno=%d,msg=%s\n",pException->msgno,pException->msg);
  }
  return 0;
}

void throwexcept(int errno)
{
  if (errno==0)
  {
    exception.msgno=100;
    strcpy(exception.msg,"I'am hungry!");
    longjmp(buf,(long)&exception);
  }
  otherexcept();
}
void otherexcept(void)
{
    exception.msgno=200;
    strcpy(exception.msg,"I need lover!");
    longjmp(buf,(long)&exception);
}

看上去是不是很相似!这两个例子中,throwexcept 函数的参数如果是0则在函数中抛出异常,如果是非0,则调用otherexcept,otherexcept直接抛出异常!otherexcpet中是两层的嵌套函数调用.
另外可以看到,在throwexcpet函数中,throw(或者longjmp)代码后面不需要return,因为执行了throw就直接返回到捕获异常调用的地方了!不知道我讲明白了没有!嘿嘿!

TOP

发新话题