应用层常见协议及其实现库
DNS
利用C语言实现DNS查询和DNS响应包的解析。
1 | /* |
HTTP
利用C语言实现HTTP请求,可以采用原生Socket往端口发包,根据HTTP协议自行构造请求的方式。
但更方便的方式是使用libcurl库。自行构造Socket请求的方式既不能保证性能,又无法保证安全。
libcurl库至少是相对完善的第三方库。
详细用法,参考libcurl官方文档以及libcurl官方样例。
SMTP
SMTP一例
1 | /* |
其他协议,后续学到再补充。
DNS
利用C语言实现DNS查询和DNS响应包的解析。
1 | /* |
HTTP
利用C语言实现HTTP请求,可以采用原生Socket往端口发包,根据HTTP协议自行构造请求的方式。
但更方便的方式是使用libcurl库。自行构造Socket请求的方式既不能保证性能,又无法保证安全。
libcurl库至少是相对完善的第三方库。
详细用法,参考libcurl官方文档以及libcurl官方样例。
SMTP
SMTP一例
1 | /* |
其他协议,后续学到再补充。
在前一夜的复习中实现了一个C/S架构的TCP套接字demo。今天首先通过创建一个UDP的套接字例子来区分UDP Socket和TCP Socket的使用区别。
客户端:
1 | /* |
服务端:
1 | /* |
本例以一个简单的http server来演示socket server构建:
1 | /* |
在阻塞的套接字中,服务端接收到的请求需要排队对其进行处理,这就形成了套接字阻塞。为了解决套接字阻塞的问题,可以创建非阻塞的服务端模型。
一个在网上找的非阻塞套接字的例子:
1 | /*************************************************************************** |
待后续补充。
Linux socket 编程,第一部分
Linux socket 编程,第二部分
Linux下的socket演示程序
编写一个单进程非阻塞多客户的套接字客户端
没有人逼你学C语言,但它在复杂的计算世界中,曲径通幽。
sockaddr_in
sockaddr_in结构体定义在/usr/include/netinet/in.h中,它的定义是这样的:
1 | /* Structure describing an Internet socket address. */ |
这个结构体中又涉及到另外几个类型和宏定义,包括__SOCKADDR_COMMON、in_port_t、in_addr。
__SOCKADDR_COMMON
__SOCKADDR_COMMON定义在bits/sockaddr.h当中,它的作用是将sin_和family参数拼接为对应的sa_family_t类型的地址家族。
1 | /* POSIX.1g specifies this type name for the `sa_family' member. */ |
也就是说这个预处理宏定义的意思是将sin_和family拼接在一起构成sa_family_t类型的sin_family。
sa_family_t就是短整形,长度16 bits的整数。
所以sockaddr_in结构体的第一个元素其实是sa_family_t类型的sin_family参数。
in_port_t
in_port_t实际上是uint16_t,16位的无符号整数。
1 | typedef uint16_t in_port_t; |
uint16_t其实就是__uint16_t,表示unsigned short int,它的长度是2个字节,能表示的范围是0-65535。
in_addr
这个显而易见,就是表示一个IPv4的地址:
1 | /* Internet address. */ |
而uint32_t也就是__uint32_t,表示unsigned int,一共4个字节。IP地址一般是拆成点分十进制看的,一个字节能表示的范围是0-255,所以的in_addr也就是能表示成0.0.0.0 ~ 255.255.255.255。
sin_zero
sin_zero的相关定义:
1 | /* Structure describing an Internet (IP) socket address. */ |
__pad声明了一个长素为8个字节的字符数组。
1 | __SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr) |
sin_family的类型
在C语言实现网络通信中,最开始需要做的就是声明并且初始化sockaddr_in结构体。
其中sin_family的类型有这些:
1 | /* Protocol families. */ |
其中最常用的AF_INET就是PF_INET,也就2所代表的IP协议家族。
sin_addr的赋值
sin_addr需要一个IPv4的地址作为值,in.h中定义了这样几个常用值,其中包括本地地址、多拨地址等:
写服务端socket最常用的INADDR_ANY地址就是((unsigned long int) 0x00000000)
,即 0.0.0.0:
1 |
|
socket的类型
关于socket的类型,在bits/socket_type.h中定义:
1 | /* Types of sockets. */ |
主要是定义在sys/socket.h中的以下函数:
关于这些函数的介绍暂不赘述,sys/socket.h里写的很详细。这里用一个socket基础的例子来演示其用法:
服务端server.c:
1 | /* |
客户端client.c:
1 | /* |
计算机堆与栈的区别就不赘述了,本内容主要通过演练几个C语言的例子来观察和探索堆栈。
一个演示栈和堆内存分配情况的例子
1 | #include <stdio.h> |
综上:
内存中的栈区主要用于分配局部变量空间,处于相对较高的地址,其栈地址是向下增长的;而堆区则主要用于分配程序员申请的内存空间,堆地址是向上增长的。
另一个关于堆栈地址分配的例子
1 | /* |
可见,程序运行时候栈底位于0x5661f000,而fmt和p这两个字符串分别存放在栈上的0x5661d0080x5661d00f和0x5661d0110x5661d015。
堆空间的分配
malloc(int)
,如果分配成功则返回分配好的地址,所以用一个指针去接收这个地址。输入参数指定分配的大小,单位是字节。
例:
char *p=(char *)malloc(100);
分配100个字节,分配的时候指定类型为char *类型,所以可以存储100个字符。
堆内存泄漏
如果在堆内存申请之后未进行安全释放,则有可能会造成内存泄漏问题。
本次复习重点不在研究堆管理机制,此部分内容留待后续再复习。
此部分内容参考资料:
Linux堆内存管理深入分析(上)
Linux堆内存管理深入分析(下)
Understanding glibc malloc
Syscalls used by malloc
把这个部分作为单独的环节来复习,是因为内联汇编在安全工程师的C语言编程当中用处是比较广泛的。无论是开发shellcode,调试exp,或是对代码做免杀,做代码虚拟机(虽然我在这方面并不擅长,但是大概看过一些其实现方式)都需要用到内联汇编。
另外对于高手来说,巧妙运用C语言内联汇编,有时是提高程序运行效率的很好的方式,或是在开发程序时,对调试起到一定的帮助。
不过,要想熟悉内联汇编,首先要熟悉汇编。
这里以Linux下,GCC作为编译器,AT&T汇编作为内联汇编格式:
简单内联汇编
1 | __asm__("汇编语句") |
拓展内联汇编
1 | __asm__ ( 汇编语句 |
简单内联汇编和拓展内联汇编的区别是简单内联汇编只包括指令,而扩展内联汇编包括操作数。
如果希望确保编译器不会在asm内部优化指令,可以在asm后使用关键字volatile。如果程序需要与 ANSI C 兼容,则应该使用 __asm__
和__volatile__
,而不是 asm 和 volatile。
通过内联汇编把a的值赋给b:
1 | #include <stdio.h> |
- 在本例中,b是输出操作数,由%0引用,a是输入操作数,由%1引用。
- “r”是操作数的约束,它指定将变量a和b存储在寄存器中。注意,输出操作数约束应该带有一个约束修饰符”=”,指定它是输出操作数。
- 要在asm内使用寄存器%eax,%eax的前面应该再加一个%,因为asm使用%0、%1等来标识变量。任何带有一个%的数都看作是输入/输出操作数,而不认为是寄存器。
- 第三个冒号后的修饰寄存器%eax告诉将在asm中修改GCC %eax的值,这样GCC 就不使用该寄存器存储任何其它的值。
movl %1, %%eax
将a的值移到%eax中,movl %%eax, %0
将%eax的内容移到b中。- 因为b被指定成输出操作数,因此当asm的执行完成后,它将反映出更新的值。换句话说,对asm内b所做的更改将在asm外反映出来。
需要注意几点第一就是所有寄存器都使用%%作为前缀,第二在这个部分新增了%0
%9的占位符来表示用户填充的数据。那么占位符,占的是什么位呢,谁来填充,怎么填充?%0%9的占位符会用输出部分和输入部分指定的寄存器或变量按照出现的顺序依次填充。如果不够填充则会出现编译错误的情况。
invalid 'asm': operand number out of range
使用内联汇编输出HelloWorld
1 | //编译方式:gcc -m32 -o test test.c |
注意
在编写内联汇编时如果遇到以下报错:
error: invalid 'asm': operand number out of range
很有可能是输入输出的操作数出了问题。说明输入输出操作数超出了范围。
在本例中,如果将输入参数从”m”类型的内存参数,改为”r”类型的寄存器参数,则会报此错。
具体原因尚在研究中。今后在使用时需要多留心参数类型的区别。
另外以上程序再编译时需要采用32位架构,否则会无法运行(因为使用的全都是32位寄存器)。
内联汇编中使用多个输入参数
1 | /*编译:gcc -m32 -o test test.c */ |
内联汇编还有很多的用法是需要琢磨的,也会随着对C语言理解的深入以及汇编的水平,探索出越来越高明的用法。
需要注意以下几点:
windows.h是Windows系统中的一个头文件,它包含了其他Windows头文件,这些头文件的某些也包含了其他头文件。这些头文件定义了Windows的所有资料型态、函数调用、资料结构和常数识别字,它们是Windows文件中的一个重要部分。C/C++ 程序在源文件前面写
#include <windows.h>
即可。
FindWindow查找窗口句柄
SendMessage向窗口句柄发送指令
1 | #include <windows.h> |
WindowFromPoint通过鼠标点击获得被点击窗口的句柄
GetCursorPos函数获取鼠标指针位置
模拟键盘向窗口发送字符:
1 | #include <windows.h> |
SetCursorPos函数设置鼠标指针位置
改变鼠标位置(运行后鼠标会飘,不失为一个小恶作剧程序):
1 | #include <windows.h> |
ShowWindow函数改变窗口状态,包括隐藏窗口
1 | #include <windows.h> |
GetClientRect函数获取窗口尺寸
1 | #include <windows.h> |
EnumWindow函数枚举遍历可见窗口
1 | #include <tchar.h> |
创建目录、拷贝文件、删除文件、删除目录
1 | #include <windows.h> |
C语言实现监听用户鼠标变动及窗口变化
1 | #include <tchar.h> |
运行结果:
延伸阅读:Windows API实现截图
https://blog.csdn.net/greenapple_shan/article/details/39828313
C语言windows.h库的常用函数(一)
C语言windows.h库的常用函数(二)
C语言windows.h库的常用函数(三)
C语言windows.h库的常用函数(四)
Win32 API Docs
Win32 API Docs / Winuser.h / EnumWindows function
本篇内容跳跃且杂乱,后续可能会再次对内容进行整理。
遍历目录
目录是一种特殊的文件,对其进行操作是,需要用到opendir、readdir、closedir等函数。
1 | #include <stdio.h> |
获取文件信息
1 | #include <stdio.h> |
解析文件描述符
Linux系统中,进程拥有各自打开文件的描述符。通常,文件描述符按生成顺序放在文件描述符表中。Linux内核将文件描述符表用一维数组描述,每个打开的文件占用一个单元,用于存放存取文件的必要信息。
信号是内核与进程间通信的一种方式。内核为每个进程定义了多种信号和处理方式,用户也可以根据需要对信号的处理方式进行重新定义。
Linux内核共定义了31种非实时的信号,为没种信号定义了处理动作,包括结束进程、写入内核文件、停止进程和忽略等。
信号默认处理方式
符号 | 含义 |
---|---|
A | 结束进程 |
B | 忽略信号 |
C | 结束进程并写入内核文件 |
D | 停止进程 |
E | 信号不能被捕获 |
F | 信号不能被忽略 |
G | 非POSIX信号 |
可靠信号与不可靠信号
因为早期历史原因,将信号值小于32的信号称为不可靠信号,32~63之间的信号称为可靠信号,也称为实时信号。
基本I/O操作模式
文件I/O操作模式
Linux下C语言进程相关的函数主要有:
函数 | 说明 |
---|---|
getpid() | 获取当前进程ID |
getppid() | 获取父进程ID |
getpgrp() | 获取进程组ID |
fork() | 创建子进程 |
wait() | 等待子进程结束 |
fork()函数是Linux系统下C语言用来创建子进程的函数。最简单的用法如下:
1 | #include <stdio.h> |
可以看到,fork()函数后的printf函数,执行了两次,一次是在进程ID为14920的进程中,打印pid的值为14930。另一次是在进程ID为14930的进程中,打印了0。
这就是fork()函数的用法,它会在程序中返回两次,在父进程中的返回值是子进程的进程ID,子进程中返回0,如此便可区分父、子进程。可以利用这个原理设计一个双进程的程序:
1 | #include <stdio.h> |
在此例中,程序是以双进程的方式在系统中执行的。并且父进程比子进程执行时间短,所以父进程先结束,子进程在父进程退出后,仍然在控制台打印出信息,是因为在父进程退出后,子进程会交由系统接管,此时子进程的父进程ID变成了一个系统进程(systemd或者init)。
C语言多进程Fork实例:
1 | #include <stdio.h> |
一个完整的多进程示例:
1 | #include <stdio.h> |
昨日家中断网,除此类突发情况和周末外,原则上不中断。
学计算机原理必须听到的一句话:线程是操作系统运算调度的最小单位。但个人觉得这句话说的不太严谨,严格意义上来说,操作系统运算调度的最小单位应该是指令。线程只能视为一系列运算指令构成的最小调度单元。
同一进程可以拥有多条线程,线程之间共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
多线程可以提高程序在多核机器上某些任务的处理性能。如果采用多进程的方式,进程间通信需要通过管道等方式,数据在用户空间和内核空间频繁切换,开销很大。而多线程因为可以使用共享的全局变量,所以通信(数据交换)非常高效。
Unix系统中,C语言线程相关主要函数定义在/usr/include/pthread.h
头文件中。其中最常用的主要有以下函数:
1 | //创建线程 |
多线程样例:
1 | #include <stdio.h> |
今晚记录的内容有点草率,一些详细的用法未能完全梳理,以后如果有机会再追加。
虽然是七夕,也还是要坚持学习呀!(老婆还没下班)
C 语言为内存的分配和管理提供了几个函数,可以在 <stdlib.h>
头文件中找到。
void *calloc(int num, int size)
num*size
个byte),并将每一个字节都初始化为 0。void free(void *address)
void *malloc(int num)
void *realloc(void *address, int newsize)
Example:
1 | #include <stdio.h> |
读文件
1 | #include <stdio.h> |
写文件
1 | #include <stdio.h> |
C语言读写二进制文件(fread,fwrite)
1 | #include <stdio.h> |
管道是在同一台计算机上两个进程之间进行数据交换的一种机制。具有单向、先进先出、无结构的字节流等特点。管道有两端,一端用于写入数据,另一端用于读出数据。
根据管道提供的接口不同,(Linux系统下)分为命名管道和无名管道。
无名管道
无名管道用于在内核中建立一条有两个端的管道,一端用于读,另一端用于写。从管道写入端写入的数据可以从读出端读出。它占用两个文件描述符,不能被非血缘关系的进程共享,一般应用在父子进程中。
1 | #include <unistd.h> |
一条命令协助理解Linux下的管道:cat /tmp/test.txt | grep XXX | more
命名管道
管道如果被命名,就可以在整个系统中使用。FIFO管道即为命名管道,它以一种特殊的文件类型存储于文件系统中,以供其他进程访问。
1 | #include <sys/types.h> |
path:管道文件的路径和名称
mode:管道文件的权限,类似open函数的第三个参数,并且自带了O_CREAT 和O_EXCL选项。
成功调用mkfifo返回0,错误返回-1。
本函数只能创建一个不存在的管道文件,或者返回”文件已存在”错误。
例:pipe_read.c
1 | #include <stdio.h> |
pipe_write.c
1 | #include <stdio.h> |
Windows下的管道
1 | #include <windows.h> |
Update your browser to view this website correctly. Update my browser now