apue学习第十一天(1)——一个C程序的存储和执行

我们把上一节提到的C程序直接扩充到一个命令处理程序的框架,它要实现的功能也就是读取分析命令,对应书中图7-9和7-11代码的合并,看下面代码:

#include "apue.h"
#include <setjmp.h>

#define TOK_ADD 5

jmp_buf jmpbuffer;

void do_line(char *);
void cmd_add(void);
int get_token(void);

int main(void)
{
	char line[MAXLINE];

	/*************/
	if( setjmp(jmpbuffer) != 0)
		printf("error");
	/*************/

	while(fgets(line, MAXLINE, sstdin) != NULL)
		do_line(line);

	exit(0);
}

char *tok_ptr;		/*global pointer for get_token()*/

void do_line(char *ptr)		/*process one line of input*/
{
	int cmd;

	tok_ptr = ptr;
	while((cmd = get_token()) > 0)
	{
		switch(cmd) {		/*one case for each command*/
			case TOK_ADD:
				cmd_add();
				break;
		}
	}

}

void cmd_add(void)
{
	int token;

	token = get_token();

	/*************/
	if(token < 0)		/*an error has occured*/
		longjmp(jmpbuffer, 1);
	/*************/
	/*rest of processing for this command*/
}

int get_token(void)
{
	/*fetch next token from line pointed by tok_ptr*/
}

这个程序很长,可能长到都没有耐心去读它。当然,其实也不需要去读它,了解到它的功能是命令处理之后,我们更关注的是几个函数之间的调用关系,并且也关注在这个程序执行过程中计算机是怎样存储它的。下面的图使得其本质一览无遗:

apue学习第十一天(1)——一个C程序的存储和执行_第1张图片

这是一幅很重要的图,接下来的叙述也是围绕着这张图展开。我们从低地址向高地址看,分别是text(正文段,可共享的,当然通常是只读的),initialized data(data段,存放初始化过的、任何函数之外的声明),uninitialized data(bss,block started by symbol,存放函数外的未初始化变量,内核将其数据初始化为0),heap,stack,命令行参数和env。a.out中还有其它类型的segment types,比如说包含symbol table、debugging info、linkage table for dynamic shared lib的那些段,但它们并不被loaded到program image中。


哪些重要呢?stack vs heap,bss vs data(到底哪些变量放在哪个区域),其实这两点我们已经模糊的了解一些,那么我们就把这最重要的放到最后说,先来把这个过程理清楚,先说说第一小点,high address的那个区域:command-line arguments and environment variables。

环境变量我们已经不陌生啦,在最初的时候我们已经配置过C_INCLUDE_PATH和LIBRARY_PATH咯。那么怎么找环境变量呢?系统提供了一个全局变量environ(environment pointer环境指针),看看它的定义吧:

extern char **environ;
有两颗星星!原谅我大惊小怪,看图就行了,没那么复杂:

apue学习第十一天(1)——一个C程序的存储和执行_第2张图片

注意,environ没有放在任何一个头文件中,所以用的时候要extern。environ是char**,environment list的起始地址是char*,当然啦,environ[0] = "HOME=/home/sar\0"; 晓得了吧。那么平时要找环境变量没那么麻烦,getenv和putenv轻松搞定;只有查看整个环境的时候才要用到environ。


另一个小点呢是上面程序中用/***********/括起来的代码,setjmp和longjmp,我们来看看它们是干什么用的。

看下面的图,我们假设这样一种情况:

apue学习第十一天(1)——一个C程序的存储和执行_第3张图片

假如最上面的那个程序调用到了cmd_add,那么这个图就是这个时候的stack frames。这时候问题来了,如果cmd_add发现了一个错误(比如一个无效的数),它希望忽略当前所读的行的剩余部分,打印一个错误,然后返回main再读取下一行继续执行。那么该怎么做呢?我们知道goto语句只能在函数内用,而这个是跨函数的!

所以说,当这种情况出现在深层次嵌套中时,我们只能用这种非局部goto的方法——setjmp和longjmp来解决啦!我们在main中set一个跳跃点,然后其他的被调用函数中出现情况时,都可以通过longjmp跳回来,longjmp(jmpbuffer, 1)中的val=1,因为val必须非0,并且这是第一个longjmp;jmpbuffer一般定义为全局变量用来恢复栈状态的所有信息。好啦,就是这个样子!


下面就来看上面提到的两点重要东西,stack vs heap,bss vs data:

1. stack vs heap

其实这两者的区别根本不用说,再清晰不过,并且以前都接触过很多遍。这里有几个相关问题很有趣:what and where the stack and heap? (http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap)。

这里着重看下heap,当然是dynamic memory allocation。下面几个函数负责memory的dynamic allocation:

#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
/*calloc() function alllocates memory for an array of nobj elements of size bytes each and returns a pointer to the allocated memory.*/
void *realloc(void *ptr, size_t newsize);
          /*All three return: non-null pointer if OK, NULL on error*/
void free(void *ptr);
首先注意void *,通用(generic)指针,所以如果#include <stdlib.h>(以获得函数原型),把它指向一个其它类型时候不需要显式的强制类型转换。realloc常用于原先空间不够时扩充空间(也可减少,但没必要)。如果heap足够新空间,那么只需要向高地址方向扩充即可;如果heap中不够新空间的分配,需要在其它地方新开辟区域,将原内容复制过去,再free原区域,返回新区域指针(值得注意的是,如果其它地方有指针指向原区域,那么这种情况下指针将不再可用,这点要注意!所以不要用指针指向malloc的区域!)

如果,malloc之后没有free,那么进程占用的memory当然会一直增加啦!这种情况叫leakage(泄露)。汗,本来要说stack vs heap的,这倒好,一直讲heap,那就这样吧!


2. bss vs data

这是initialized和uninitialized变量存放区域之争。

突然发现这儿不仅要分析各种变量的类型,还要指明他们的存储方式,更要洞悉它们在程序里的变化,太重要啦!

面对即将到来的如此精彩的内容,我决定开一个新的篇章写这些,快转向下一节!


——————————————————————————————————————————————————

嗨,别急,还有几个有意思的课后题。

7.2 看下面一段代码,特别有意思:

#include <stdio.h>
/*line buffer example: if code before the "sleep" is short and there's no '\n' in it, then the line buffer is not full and thus it will print on the screen until the '\n' behind "sleep";*/

int main()
{

	int i = printf("hello, ");	
	sleep(3);
	int j = printf("world");
	int x = printf("\n");

	printf("%d\n", i);
	printf("%d\n", j);
	printf("%d\n", x);

	return 0;
}

虽然“hello, ”在sleep之前,但它不会输出,等到找到了那个'\n'的时候才会输出。这就能看出pringf是行缓冲的,很神奇吧!

等等,还有更有意思的,exit()可以flush buffer,_exit却不flush buffer,所以如果把return 0换成了:printf("can I output?"); _exit(0); 这句话绝对输不出来,不信你试试。


7.7 如下命令为啥不输出stack和heap的大小?

liuzch@liuzch:~/workspace$ size /usr/bin/cc /bin/sh
   text	   data	    bss	    dec	    hex	filename
 346919	   3576	   6680	 357175	  57337	/usr/bin/cc
 102134	   1776	  11272	 115182	  1c1ee	/bin/sh
看,text, data, bss大小都有,后面两个是十进制和十六进制的大小,但是为什么没stack和heap呢?原因是,stack和heap在exec后才会分配嘛!


ok,结束。







你可能感兴趣的:(apue学习第十一天(1)——一个C程序的存储和执行)