GCC(GNU Compiler Collection,GNU编译器套装),原名为GNU C语言编译器(GNU C Compiler),只能处理C语言。但其很快扩展,变得可处理C++,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。
首先,讨论一下,什么是编译器?
编译器是用于将高级语言代码翻译成机器语言的程序。那什么是高级语言和机器语言?机器语言Machine Language是一种低级语言。机器语言是计算机唯一能接受和执行的语言。机器语言由二进制码组成,每一串二进制码叫做一条指令。一条指令规定了计算机执行的一个动作。一台计算机所能懂得的指令的全体,叫做这个计算机的指令系统。不同型号的计算机的指令系统不同。
指令是用0和1组成的一串代码,它们有一定的位数,并分成若干段,各段的编码表示不同的含义,例如某台计算机字长为16位,即有16个二进制数组成一条指令或其它信息。16个0和1可组成各种排列组合,通过线路变成电信号,让计算机执行各种不同的操作。
如某种计算机的指令为1011011000000000,它表示让计算机进行一次加法操作;而指令1011010100000000则表示进行一次减法操作。它们的前八位表示操作码,而后八位表示地址码。从上面两条指令可以看出,它们只是在操作码中从左边第0位算起的第6和第7位不同。这种机型可包含256(=2^8)个不同的指令。
机器语言或称为二进制代码语言,计算机可以直接识别,不需要进行任何翻译。每台机器的指令,其格式和代码所代表的含义都是硬性规定的,故称之为面向机器的语言,也称为机器语言。它是第一代的计算机语言。机器语言对不同型号的计算机来说一般是不同的。使用机器语言编写程序是一种相当烦琐的工作,既难于记忆也难于操作,编写出来的程序全是由0和1的数字组成,直观性差、难以阅读。不仅难学、难记、难检查、又缺乏通用性,给计算机的推广使用带来很大的障碍。
汇编语言Assembler Language(低级语言)
为了克服机器语言上述的缺点,很快就出现汇编语言。用能反映指令功能的助记符表达的计算机语言叫汇编语言。它是符号化了的机器语言。用汇编语言编写的程序叫汇编语言源程序,计算机无法执行。必须用汇编程序把它翻译成机器语言目标程序,计算机才能执行。这个翻译过程称为汇编过程。
汇编语言是用助记符表示指令功能的计算机语言。与机器语言相比,汇编语言具有以下的几个特点:第一,它使用符号来表示操作码和地址码,这种符号便于记忆,称为记忆码。第二,汇编程序自动处理存储分配,毋需程序员做存储分配工作。第三,程序员可以直接书写十进制数。
例如,要计算c=7+8,可以用如下几条汇编命令:
标号 指令 说明
START GET 7; 把7送进累加器ACC中
ADD 8; 累加器ACC+8送进累加器ACC中
PUT C; 把累加器ACC送进C中
END STOP; 停机
使用汇编语言来编写程序或阅读已经编写好的程序比起机器语言来要简单和方便多了。这就是计算机语言发展中的第二代语言。人们使用这种助记符编写程序后,要是计算机能够接受,还必须把编好的程序逐条翻译成二进制编码的机器语言。当然,这个工作并不是由程序员来完成,而是有称为“汇编程序”的程序自动完成的。汇编程序的功能就是把由汇编语言编写的程序(称为汇编语言源程序)翻译成机器语言程序,计算机才能执行该程序。这个翻译过程称为汇编。
汇编语言比起机器语言在很多方面都有很大的优越性,如编写容易、修改方便、阅读简单、程序清楚等,但在计算机语言系统中,把汇编语言仍然列入“低级语言”的范畴,它仍然是属于面向机器的语言,也就是说,不同的计算机可以有不同的指令集。
高级语言(High-level language)
机器语言和汇编语言都是面向机器的,高级语言是面向用户的。到了50年代中期,出现程序设计的高级语言如Fortran、Algol60,以及后来的PL/l、Pascal,再到C、C++、Go等,算法的程序表达才产生一次大的飞跃。用高级语言编写的程序叫做高级语言源程序,必须翻译成机器语言目标程序才能被计算机执行。
程序设计语言从机器语言到高级语言的抽象,带来的主要好处是:
- 高级语言接近算法语言,易学、易掌握,一般工程技术人员只要几周时间的培训就可以胜任程序员的工作;
- 高级语言为程序员提供了结构化程序设计的环境和工具,使得设计出来的程序可读性好,可维护性强,可靠性高;
- 高级语言远离机器语言,与具体的计算机硬件关系不大,因而所写出来的程序可移植性好,重用率高;
由于把繁杂琐碎的事务交给了编译器去做,所以自动化程度高,开发周期短,且程序员得到解脱,可以集中时间和精力去从事更为重要的创造性劳动,以提高程序的质量。
GCC使用
我们将从头开始一步一步地做,以便理解编译过程,了解为了制作可执行文件需要做些什么,按什么顺序做。步骤(以及所用工具)如下: 预编译 (gcc -E)、 编译 (gcc)、 汇编 (as)以及连接 (ld)。
首先,我们应该知道如何调用编译器。实际上,这很简单。从hello world程序开始。
#include <stdio.h>
int main()
{
printf("Hello World!/n");
}
把这个文件保存为 hello.c。 在命令行下编译它:
gcc hello.c
在默认情况下,C编译器将生成一个名为 a.out 的可执行文件。可以键入如下命令运行它:
./a.out
每一次编译程序时,新的 a.out 将覆盖原来的程序。你无法知道是哪个程序创建了 a.out。我们可以通过使用 -o 编译选项,告诉 gcc我们想把可执行文件叫什么名字。我们将把这个程序叫做 game,我们可以使用任何名字,因为C没有Java那样的命名限制。
gcc -o hello hello.c
./hello
Hello World
gcc编译程序过程:
在使用gcc编译程序时,编译过程可以为4个阶段:
(1)预处理:(Pre-Processing)
(2)编译:(Compiling)
(3)汇编:(Assembling)
(4)链接:(Linking)
1. 预处理(Preprocess):以源文件作为输入,删除其中的注释,解析其中以#开头的行(#include, #define, #if/ifdef/ifndef/elif/else 等),输出预处理后源文件(.c)
2. 编译(Compile):以预处理后源文件作为输入,经过词法分析,语法分析,语义分析,中间代码生成与优化,目标代码生成,输出与处理器体系(x86/arm)相关的汇编源文件(.s)
3. 汇编(Assemble):以汇编源文件作为输入,执行汇编,生成与操作系统相关的目标文件(.o, .obj)
4. 链接(Link):以目标文件(全部)、库文件为输入,解析未定义的符号引用,将目标文件中的占位符替换为符号的地址,完成程序中各目标文件的地址空间的组织(重定位),输出可执行二进制文件
gcc的常用选项
在使用gcc编译器的时候,我们必须给出一系列必要的选项和文件名。gcc编译器的选项有100多个,其中很多参数一般是用不到的。另外,我们可以通过使用man gcc / info gcc来详细了解gcc的所有选项。
gcc [options] [filenames]
其中options就是编译器所需要的选项,filenames给出相关的文件名。
- -c:只编译,不链接生成可以执行文件,编译器值是由输入的.c等为后缀的源文件生成.o为后缀的目标文件,通常用于编译不包含主程序的子程序文件。
- -o output_file:确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就默认将输出的可执行文件命名为a.out。
- -g:产生调试器gdb所必须的符号信息,要对源代码进行调试,就必须在编译程序是加入这个选项。
- -O:对程序进行优化编译、链接,采用这个选项,整个源代码会在编译、链接过程中进行优化处理,这样产生的可执行文件效率较高,但是,编译、链接的速度就相应地要慢一些。
- -O2:比-O更好的优化编译、链接,当然整个编译、链接过程会更慢。
- -Wall:输出所有警告信息,在编译的过程中如果gcc遇到一些认为可能会发生错误的地方就会提出一些相应的警告和提升信息。提升注意这个地方是不是有什么错误。
- -w:关闭所有的警告,建议不要使用此选项。
使用-E选项
可以使用vi命令查看hello.i的结果。
gcc -S hello.i -o hello.s
gcc -c hello.s –o hello.o
gcc hello.o –o hello
最终生成了可执行的代码hello。
接下来,我们用C语言写一点代码,结合C语言来学习一下网络知识。
套接字(socket)允许在相同或不同的机器上的两个不同进程之间进行通信。更准确地说,它是使用标准Unix文件描述符与其他计算机通信的一种方式。在Unix中,每个I/O操作都是通过写入或读取文件描述符来完成的。文件描述符只是与打开文件关联的整数,它可以是网络连接、文本文件、终端或其他内容。
对于程序员来说,套接字的使用和行为很像更底层的文件描述符。这是因为对于套接字,read()和write()等命令可以像在文件和管道编程中同样的使用。
套接字首先在BSD 2.1中引入,然后在BSD 4.2形成当前的稳定版本。现在,大多数最新的UNIX系统版本都提供了套接字功能。
套接字在哪里使用?
Unix Socket用于客户端 - 服务器应用程序框架中。服务器是根据客户端请求执行某些功能的过程。大多数应用程序级协议(如FTP、SMTP和POP3)都使用套接字在客户端和服务器之间建立连接,然后交换数据。
以及这张图:
套接字类型
用户可以使用四种类型的套接字。前两个是最常用的,后两个使用较少。一般假定进程仅在相同类型的套接字之间进行通信,但是也没有限制阻止不同类型的套接字之间的通信。
- 流(stream)套接字 - 在网络环境中保证交付。如果通过流套接字发送三个项目“A,B,C”,它们将以相同的顺序 - “A,B,C”到达。这些套接字使用TCP(传输控制协议)进行数据传输。如果无法交付,发件人会收到错误提示。
- 数据报(Datagram)套接字 - 无法保证在网络环境中交付。它们是无连接的,因为不需要像流套接字那样打开连接 ,使用UDP(用户数据报协议)。
- 原始(raw)套接字 - 使用原始套接字,用户可以访问底层通信协议,这些协议支持套接字抽象。这些套接字通常是面向数据报的,但它们的确切特性取决于协议提供的接口。原始套接字不适用于普通用户;它们主要是为那些有兴趣开发新通信协议的人提供的,或者是为了获得对现有协议的一些不常见的使用。
- 顺序数据包(Sequenced Packet)套接字 - 类似于流套接字。此接口仅作为网络系统(NS)套接字抽象的一部分提供,在大多数NS应用程序中非常重要。顺序数据包套接字允许用户通过编写原型标头以及要发送的任何数据来操作数据包或一组数据包上的序列数据包协议(SPP)或Internet数据报协议(IDP)标头,或者通过指定要与所有传出数据一起使用的默认标头,并允许用户在传入数据包上接收标头。
套接字如何使用
使用socket的时候需要使用各种结构来保存有关地址和端口的信息以及其他信息。 大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。通常使用四元组<源ip,源port,目的ip,目的port>来描述一个网络连接,使用socket的时候,往往也需要数据结构来描述这些信息。
第一个数据结构是sockaddr:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
这是一个通用的套接字地址结构,在大多数套接字函数调用中都需要使用它。 成员字段的说明如下。sa_family包括以下可选值。每个值代表一种地址族(address family),在基于IP的情况中,都使用AF_INET。
- AF_INET
- AF_UNIX
- AF_NS
- AF_IMPLINK
sa_data长为14字节,根据地址类型解释协议特定地址。 对于Internet系列,我们将使用端口号+IP地址,该地址由下面定义的sockaddr_in结构表示。
第二个数据结构是sockaddr_in:
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
其中,sin_family和sockadd的sa_family一样,包括四个可选值:
- AF_INET
- AF_UNIX
- AF_NS
- AF_IMPLINK
sin_port是端口号,16位长,网络字节序(network byte order);sin_addr是IP地址,32位长,网络字节序(network byte order)。sin_zero,8个字节,设置为0。
至于为何会使用两个数据结构sockaddr和sockaddr_in来表示地址,原因是如sa_family所指出的,socket设计之初本来就是准备支持多个地址协议的。不同的地址协议由自己不同的地址构造,譬如对于IPv4就是sockaddr_in
, IPV6就是sockaddr_in6
, 以及对于AF_UNIX就是sockaddr_un
。sockaddr是对这些地址的上一层的抽象。另外,像sockaddr_in将地址拆分为port和IP,对编程也更友好。这样,在讲所使用的的值赋值给sockaddr_in数据结构之后,通过强制类型转换,就可以转换为sockaddr。当然,从sockaddr也可以强制类型转换为sockaddr_in。
在sockaddr_in中还有一个结构体,struct in_addr,
struct in_addr {
unsigned long s_addr;
};
就是一个32位的IP地址,同样是网络字节序。
关于字节序,补充一些内容:
- Little Endian - 在该方案中,低位字节存储在起始地址(A)上,高位字节存储在下一个地址(A + 1)上。
- Big Endian - 在该方案中,高位字节存储在起始地址(A)上,低位字节存储在下一个地址(A + 1)上。
为了允许具有不同字节顺序约定的机器相互通信,Internet协议为通过网络传输的数据指定了规范的字节顺序约定。 这称为网络字节顺序。在建立Internet套接字连接时,必须确保sockaddr_in结构的sin_port和sin_addr成员中的数据在网络字节顺序中表示。
不用担心这几个数据结构以及字节序,因为socket接口非常贴心地准备好了各种友好的接口。
- htons() Host to Network Short
- htonl() Host to Network Long
- ntohl() Network to Host Long
- ntohs() Network to Host Short
譬如对上面描述的过程,想要把地址200.200.200.200和端口3456绑定到一个socket,以下代码就足够了:
struct sockaddr_in myaddr;
int s;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(3456);
inet_aton("200.200.200.200", &myaddr.sin_addr.s_addr);
s = socket(PF_INET, SOCK_STREAM, 0);
bind(s, (struct sockaddr*)myaddr, sizeof(myaddr));
下面会看到,对于简单的socket应用编程,所需要做的就是记住流程。
使用客户端-服务器端(client-server)模型作为一个例子。server一般打开端口,被动侦听,不需要知道客户端的IP和端口;而client发起请求,必须知道服务器端的IP和端口。
在这个过程中,所需要用到的函数如下:
再用一张图描述下客户端和服务器端的流程:
接下来,我们看C/S的代码实例。
客户端代码:
#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include <errno.h>
#include <stdlib.h>
int main(){
int clientfd, conn;
struct sockaddr_in servaddr,cliaddr;
char buff[1024];
char buff2[1024];
int servlen;
int n;
bzero(buff,1024);
bzero(buff2,1024);
bzero(&cliaddr,sizeof(cliaddr));
cliaddr.sin_addr.s_addr = htonl(INADDR_ANY);
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(0);
clientfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
if(inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr)<0) printf("address error1n");
//if(inet_pton(AF_INET,"192.168.116.158",&servaddr.sin_addr)<0) printf("address error1n");
servaddr.sin_port = htons(2345);
servlen = sizeof(servaddr);
conn = connect(clientfd,(struct sockaddr *)&servaddr,servlen);
if(conn < 0) printf("connect error!n");
if(n=recv(clientfd,buff2,sizeof(buff2),0)>0)
printf("Message %s:",buff2);
printf("clientfd is %d,connfd is %d.n",clientfd,conn);
while(1){
while((n=read(0,buff,sizeof(buff)))>0){
if(send(clientfd,buff,n,0)<0)
{printf("send error! %s(errno :%d)n",strerror(errno),errno);
exit(0);}
if((n=recv(clientfd,buff2,sizeof(buff2),0))>0){
write(0,buff2,n);
}
}
}
close(clientfd);
}
以及服务器端代码:
#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include <errno.h>
#include <stdlib.h>
int main(){
int listenfd, connfd;
struct sockaddr_in servaddr,cliaddr;
char buff[1024];
int clilen;
int n;
listenfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(2345);
if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)
printf("bind error!n");
listen(listenfd,10);
clilen = sizeof(cliaddr);
printf("serverfd is %d, connfd is %d.n",listenfd,connfd);
while(1){
connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&clilen);
send(connfd,"Welcome to Server!n",19,0);
while((n=read(connfd,buff,sizeof(buff)))>0){
// printf("Received string length is %d.n",n);
write(1,buff,n);
n = read(0,buff,sizeof(buff));
write(connfd,buff,n);
}
close(connfd);}
close(listenfd);
}
编译之后,就可以在两个进程间进行通信了。这个简单代码的作用是让客户端和服务器端进行通信。