0%

HTTP/1.1

优点

  1. 简单
    HTTP 基本的报文格式就是 header + body,头部信息也是 key-value 简单文本的形式,易于理解,降低了学习和使用的门槛。

  2. 灵活和易于扩展

    HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员

    自定义和扩充

    同时 HTTP 由于是工作在应用层( OSI 第七层),则它下层可以随意变化,比如:

    • HTTPS 就是在 HTTP 与 TCP 层之间增加了 SSL/TLS 安全传输层;
    • HTTP/1.1 和 HTTP/2.0 传输协议使用的是 TCP 协议,而到了 HTTP/3.0 传输协议改用了 UDP 协议

缺点

  1. 无状态:双刃剑
    • 好处:服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务
    • 坏处:在完成有关联性的操作时会非常麻烦。例如:登录->添加购物车->下单->结算->支付,这系列操作都要知道用户的身份才行。但服务器不知道这些请求是有关联的,每次都要问一遍身份信息

解决方案:Cookie 技术
相当于在客户端第一次请求后,服务器会下发一个装有客户信息的「小贴纸」,后续客户端请求服务器的时候,带上「小贴纸」,服务器就能认得用户

  1. 不安全:
    • 通信使用明文(不加密),内容可能会被窃听
    • 不验证通信方的身份,因此有可能遭遇伪装。比如,访问假的淘宝、拼多多,那你钱没了
    • 无法证明报文的完整性,所以有可能已遭篡改。

解决方案:HTTPS,也就是通过引入 SSL/TLS 层

性能

HTTP 协议是基于 TCP/IP,并且使用了「请求 - 应答」的通信模式

  1. 长连接
    • HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手)
    • HTTP/1.1 提出了长连接的通信方式,也叫持久连接,特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态
  2. 管道网络传输
    • 同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间
    • 举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。那么,管道机制则是允许浏览器同时发出 A 请求和 B 请求
    • 但是服务器必须按照接收请求的顺序发送对这些管道化请求的响应;如果服务端在处理 A 请求时耗时比较长,那么后续的请求的处理都会被阻塞住,这称为「队头堵塞」。所以,HTTP/1.1 管道解决了请求的队头阻塞,但是没有解决响应的队头阻塞

实际上 HTTP/1.1 管道化技术不是默认开启,而且浏览器基本都没有支持

  1. 队头阻塞:「请求 - 应答」的模式会造成 HTTP 的性能问题
    当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」

总结:HTTP/1.1 的性能一般,后续的 HTTP/2 和 HTTP/3 就是在优化 HTTP 的性能

防火墙实现基础:Netfilter机制

Netfilter机制的核心是一个开放式的IP数据包处理框架,对外提供了处理IP数据包的统一接口,编程人员可以利用该接口实现对IP数据包的控制和其他新的处理方式

1.Netfilter机制的运行原理

核心思想:在网络IP协议层的IP数据包处理流程中,总结出几个关键点(即钩子点),提供了多种可能的IP数据包处理方式和开放接口。安全管理员不仅可以配置Netfilter以不同的处理方式处理IP数据包,也可以利用Netfilter所提供的开放接口在IP数据包的协议处理流程中实现新的IP数据包处理方式
在Netfilter机制中有五个钩子点:

  1. IP_PRE_ROUTING:ip数据包刚刚从网络接口接收到
  2. IP_FORWARD:ip数据包已经进行了路由处理,需要转发到下一跳但还没有进行转发
  3. IP_LOCAL_IN:从网络接收的需要发往本机的数据包
  4. IP_LOCAL_OUT:刚刚从本机发出还未进行路由处理的ip数据包
  5. IP_POST_ROUTING:ip数据包将要离开本机发往下一跳之前

2.Netfilter功能种类

Netfilter在这些钩子点上所支持的不同报文处理方式可以分为两种:
1.内嵌处理方式:不用自己编写程序,通过配置Netfilter就可以实现相应的报文处理方式

  • 报文过滤方式
  • 报文重定向
    2.开放处理方式:在Netfilter基础上自己开发新的报文处理,Netfilter将数据包的处理权交给新开发的报文处理
  • 钩子函数方式:在流过某钩子点时,注册在该钩子点的钩子函数将会被调用
    • 内核模块可以在这些钩子点上注册钩子函数,将自定义的IP数据包处理函数挂接在相应的钩子上。
    • 当数据包经过Netfilter框架的某个钩子点时,内核会检测是否有内核模块对该钩子点进行了钩子函数注册。如果有注册,就调用相应的钩子函数,从而有机会检查和修改数据包,并返回处理结果给Netfilter框架。
    • Netfilter根据钩子函数的返回结果来处理IP数据包,如丢弃数据包或按常规处理数据包。
    • 多个内核模块可以在同一个钩子点注册它们的钩子函数,Netfilter会按照优先级调用这些钩子函数。
  • 队列输出方式:钩子函数方式需要进行复杂的Linux内核编程,对于安全管理员来说具有挑战性。
    • Netfilter提供了队列输出功能,使得经过的IP数据包可以直接交给应用层处理。
    • 安全管理员可以在应用层开发应用软件,对数据包进行自主处理,如丢弃或修改。
    • 处理后的数据包可以再通过Netfilter的队列输出功能发送给IP协议层进行后续协议处理。

网络防火墙功能与结构解析

1.网络防火墙的基本概念

网络防火墙一般部署在内部网络接入Internet的出口处,通过对网间所传递的数据进行分析和控制,只有被认定为正常的网络协议数据才能通过防火墙

2.网络防火墙的网络访问控制功能

网络防火墙最核心的基本功能:网络访问的管理和控制
实现关键:甄别网络访问是否正当——访问控制的决策过程,涉及到三方面:

  1. 访问控制规则的设计
  2. 访问控制决策的形成
  3. 访问控制决策的实施

3.访问控制功能的实现要素

网络防火墙完成完整的访问控制功能,需要三个基本的功能要素:

  1. 访问控制规则的配置
  2. 基于访问控制规则的访问判决
  3. 访问判决的实施

3.1 访问控制规则的配置

访问控制规则所依赖的要素:

  1. 网络访问参量:网络访问属性,例如数据包的源ip、源端口、目的ip等
  2. 系统状态参量:全局性的系统状态变量,例如系统时间:工作日的9:00~17:00才能访问
  3. 自定义参量

访问控制规则=多条规则组成的规则集合——>规则冲突解决方案

3.2 基于访问控制规则的访问判决

根据访问属性执行相应的访问控制规则,得出具体的访问判决结构
基于访问控制规则的访问判决包含三个过程:

  1. 控制规则选取阶段:预先知道网络访问的一些属性,才能选取相应的控制规则,比如说两条访问控制规则,分别用于TCP应用和UDP应用,则得先知道网络访问对应于哪种业务类型,tcp还是udp

  2. 单规则判决阶段:

    如何获得该规则中的参量:

    1. 读取配置文件…
    2. 全局状态参量:访问全局数据结构或状态查询函数
    3. 网络访问参量值
  3. 冲突判决仲裁阶段:判决结果冲突,大多数防火墙采取“否定优先”的方式

3.3 网络访问判决的实施

要对一个网络访问进行控制,首先需要在机制上保证网络防火墙能够截获到该网路访问,如截获一个TCP连接。也就是说,网络防火墙必须运行在网络访问所经的结点或者至少有一个模块运行在相应的节点上。
详细见3.5

4.网络防火墙的逻辑结构

网络防火墙在逻辑上可以分为三大模块:

  1. 访问控制规则配置模块:提供一个接口,供网络管理员配置相应的访问控制规则,并将配置结果保存在访问控制规则库中
  2. 网络访问截获和控制模块:截获网络访问,向网络访问判决模块询问如何处理该网络访问,待网络访问判决模块返回判决结果后,依据所得到的判决结果对该网络访问实施访问控制,放行还是阻断该网络访问
  3. 网络访问判决模块:针对网络访问截获和控制模块提交来的访问判决请求,依据访问控制规则形成访问判决,并将判决结果返回给网络访问截获和控制模块。
    此外还需要一个保存访问控制规则的数据库

HTTP

HTTP基本概念

HTTP是什么

HTTP 超文本传输协议

  1. 传输:两点之间,可以是本地浏览器和服务器,也可以是服务器和服务器
  2. 超文本:文字、图片、视频等文本的混合体,并且有超链接,能从一个超文本跳转到另一个超文本

HTTP常见状态码

五大类HTTP状态码

  1. 1XX:提示信息,表示协议处理的一种中间状态

  2. 2XX:报文已经被收到并被正确处理

    • 200 OK:表示一切正常,如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
    • 204 No Content:与 200 OK 基本相同,但响应头没有 body 数据
    • 206 Partical Content:应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。
  3. 3XX:客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向

    • 301 Moved Permanently:永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问
    • 302 Found:临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问

    301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL

    • 304 Not Modified: 不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向(返回客户端上的缓存),也就是告诉客户端可以继续使用缓存资源,用于缓存控制
  4. 4XX:客户端发送的报文有误,服务器无法处理

    • 400 Bad Request:客户端请求的报文有误,笼统的错误码
    • 403 Forbidden:客户端请求的资源是服务器禁止访问的
    • 404 Not Found:请求的资源的在服务器上不存在或未找到
  5. 5XX:客户端请求报文正确,但是服务器处理时内部发生错误

    • 500 Internal Server Error:笼统的错误码,表示服务器发生错误,发生什么错误不知道
    • 501 Not Implemented:客户端请求的功能还不支持,“即将开业,敬请期待”
    • 502 Bad Gateway:服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误
    • 503 Service Unavailable:表示服务器当前正忙,暂时无法响应,“网络服务正忙,请稍后再试”

HTTP常见字段

  • host:客户端发送请求时,指定服务器的域名
  • Content-Length:本次回应数据的长度,解决“粘包”

HTTP协议解决粘包的方法:1. 设置回车符、换行符作为http header的边界;2.通过Conten-Length字段作为http body的边界

  • Connection:用于客户端要求服务器使用“HTTP长连接”机制,以便其他请求复用

HTTP长连接的特点:只要任意一端没有明确提出断开连接,则保持TCP连接状态
HTTP/1.1版本默认使用长连接,为了兼容旧版本,所以设定Connection字段的值为Keep-Alive
ps:注意 HTTP Keep-Alive 和 TCP Keepalive 不是一个东西

  • Accept:客户端请求的时候,声明自己可以接受哪些数据格式,Accept: */*:客户端声明自己可以接受任何格式的数据
  • Content-Type:服务器回应时,告诉客户端,本次数据是什么格式,Content-Type: text/html; Charset=utf-8:表明,发送的是网页,而且编码是UTF-8
  • Accept-Encoding:客户端在请求时,说明自己可以接受哪些压缩方法,Accept-Encoding: gzip, deflate
  • Content-Encoding:说明数据的压缩方法,表示服务器返回的数据使用了什么压缩格式,Content-Encoding: gzip

GET与POST

GET和POST的区别

  • GET和POST的语义区别

    (根据RFC规范)

    • GET:从服务器获取指定的资源,请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。
    • POST:根据请求负荷(报文body)对指定的资源做出处理,POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制

安全和幂等的概念:

  • 安全:HTTP协议里所谓的安全是请求方法不会破坏服务器上的资源
  • 幂等:多次执行相同的操作,结果都是相同的
  • GET 和 POST 方法是否安全和幂等

    (RFC 规范定义)

    • GET 方法就是安全且幂等的,因为它是只读操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签
    • POST 因为是新增或提交数据的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。所以,浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签。

注意: 上面是从 RFC 规范定义的语义来分析的,但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:

  • 可以用 GET 方法实现新增或删除数据的请求,这样实现的 GET 方法自然就不是安全和幂等。
  • 可以用 POST 方法实现查询数据的请求,这样实现的 POST 方法自然就是安全和幂等。

Q:GET 请求可以带 body 吗?
A:RFC 规范并没有规定 GET 请求不能带 body 的。理论上,任何请求都可以带 body 的。只是因为 RFC 规范定义的 GET 请求是获取资源,所以根据这个语义不需要用到 body。

另外,URL 中的查询参数也不是 GET 所独有的,POST 请求的 URL 中也可以有参数的

HTTP缓存技术

HTTP缓存的实现方式

重复性的HTTP请求,每次都会得到相同的结果——>[请求-响应]——>缓存在本地,下次直接从缓存读取,不用通过网络从服务器获取,提升性能
HTTP缓存的实现方式:1.强制缓存;2.协商缓存

强制缓存

强制缓存:浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,主动权在浏览器
实现字段:
Cache-Control:相对时间,优先级更高,建议使用
Expires:绝对时间
实现流程:

  1. 浏览器第一次请求访问时,服务器返回资源的同时,在响应报文头部加上Cache-Control,设置了过期时间的大小
  2. 浏览器再次请求访问资源时,先通过请求资源的时间和过期时间,判断资源是否过期,如果没过期就使用缓存,否则重新请求服务器
  3. 服务器再次返回资源时,会更新response头部的Cache-Control
协商缓存

响应码304:告诉浏览器可以使用本地缓存
协商缓存:通过服务器告知客户端是否可以使用缓存,即浏览器和服务端协商之后,通过协商结果判断是否使用本地缓存
注意:协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。
实现字段:

  1. 请求头部中的 If-Modified-Since 和响应头部中的 Last-Modified 字段实现
    a. If-Modified-Since:资源过期,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 缓存。
    b. Last-Modified:响应资源的最后修改时间

2.请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段
a. ETag:唯一标识响应资源
b. If-None-Match:资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

如果在第一次请求资源的时候,服务端返回的 HTTP 响应头部同时有 Etag 和 Last-Modified 字段,那么客户端再下一次请求的时候,如果带上了 ETag 和 Last-Modified 字段信息给服务端,这时 Etag 的优先级更高,也就是服务端先会判断 Etag 是否变化了,如果 Etag 有变化就不用在判断 Last-Modified 了,如果 Etag 没有变化,然后再看 Last-Modified。

为什么 ETag 的优先级更高?
这是因为 ETag 主要能解决 Last-Modified 几个比较难以解决的问题:

  1. 在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
  2. 可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次;
  3. 有些服务器不能精确获取文件的最后修改时间

使用 ETag 字段实现的协商缓存的过程:

  1. 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;
  2. 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:
    • 如果没有过期,则直接使用本地缓存;
    • 如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;
  3. 服务器再次收到请求后,会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较:
    • 如果值相等,则返回 304 Not Modified,不会返回资源;
    • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
  4. 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源

字典结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dict = {
"family": {
"grandfather": [
"John",
{
"father": "Michael",
"kids": [
{"kid": ("Tom", 10, "M", "123 Street")},
{"kid": ("Lucy", 12, "F", "456 Street")}
]
}
]
}
}

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def compare_dicts(dict1, dict2, path=""):
differences = []

# Check for keys in dict1 not in dict2
for key in dict1:
if key not in dict2:
differences.append(f"Key {path + str(key)} is missing in the second dictionary")
else:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
differences.extend(compare_dicts(dict1[key], dict2[key], path + str(key) + "->"))
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
if len(dict1[key]) != len(dict2[key]):
differences.append(f"List length mismatch at {path + str(key)}")
else:
for i, (item1, item2) in enumerate(zip(dict1[key], dict2[key])):
if isinstance(item1, dict) and isinstance(item2, dict):
differences.extend(compare_dicts(item1, item2, path + str(key) + f"[{i}]->"))
elif item1 != item2:
differences.append(f"List item mismatch at {path + str(key)}[{i}]: {item1} != {item2}")
elif isinstance(dict1[key], tuple) and isinstance(dict2[key], tuple):
if dict1[key] != dict2[key]:
differences.append(f"Tuple mismatch at {path + str(key)}: {dict1[key]} != {dict2[key]}")
else:
if dict1[key] != dict2[key]:
differences.append(f"Value mismatch at {path + str(key)}: {dict1[key]} != {dict2[key]}")

# Check for keys in dict2 not in dict1
for key in dict2:
if key not in dict1:
differences.append(f"Key {path + str(key)} is missing in the first dictionary")

return differences

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def main():
dict1 = {
"family": {
"grandfather": [
"John",
{
"father": "Michael",
"kids": [
{"kid": ("Tom", 10, "M", "123 Street")},
{"kid": ("Lucy", 12, "F", "456 Street")}
]
}
]
}
}

dict2 = {
"family": {
"grandfather": [
"John",
{
"father": "Michael",
"kids": [
{"kid": ("Tom", 10, "M", "123 Street")},
{"kid": ("Lucy", 13, "F", "456 Street")}
]
}
]
}
}

differences = compare_dicts(dict1, dict2)
if not differences:
print("没有差异")
else:
print("差异如下:")
for diff in differences:
print(diff)

if __name__ == "__main__":
main()

Linux内核级安全开发基础

1.2 内核模块开发方法

1.源代码组成

Linux动态内核模块一般需要包括三部分

  1. 模块初始化部分:模块的注册、数据和变量的初始化

    该模块以初始化函数的形式存在,在模块加载到内核运行时,系统就会自动调用该函数完成模块的初始化

  2. 模块的注销部分:完成资源的释放

    该部分以注销函数的形式出现,在模块从内核卸载时,系统就会自动调用该函数完成模块的注销工作

  3. 模块的主体功能部分:用于实现模块的具体功能

    该部分通常以一组函数形式存在,不会自动运行,在需要的时候由用户通过系统调用或其他的功能模块调用

2.外部符号引用
  • 应用程序vs内核模块的外部资源

    应用程序的开发通常需要调用一些外部的资源,如库函数、系统调用等,以实现较为复杂同样 Linux的内核模块编程也经常需要使用外部资源,不同的是应用程序中使用的外部资源是库函数或者系统调用,而对内核模块而言,要使用的外部资源是在 Linux基本内核或其他内核模块中定义的资源(函数、全局变量等)等。

  • 与应用程序编程所使用的库函数不同,基本内核或其他内核模块中定义的外部符号(简称为内核符号)缺乏相应的使用文档。比如 Linux 中可以通过 man(或 info)命令列出一个库函数详细的使用文档,包括函数名、兩数功能、返回值类型、参数个数,以及每个参数的类型等。而 Linux 中的内核符号没有详细的使用文档,这给内核模块开发人员造成了很大困难。

3.编译和运行模式

运行环:x86系列的处理器而言,存在4种运行模式,每种模式对应不同的执行权限级别,按执行权限级别从大到小的次序依次为0、1、2、3,由于这些级别的执行权限像同心圆一样存在严格的包含关系,这些执行权限级别常被人称为运行环,即0环至3环共4个环。

  • 当CPU运行于0环时具有最高的权限,能够执行所有的指令和特权操作,如访问控制寄存器等,运行于3环时则权限最低,无法执行任何特权指令,也无法访问硬件。在这些运行环中,通常只用到两个环,即0环和3环,并将 CPU 运行在0环时称为 CPU 处于特权态,而将CPU 运行在3环时称为 CPU 处于非特权态。
  • 操作系统的内核代码运行在特权态,特权态又称管态、系统态或内核态,而让运行在操作系统之上的应用程序代码运行在非特权态,该态一般又称为目标态或用户态。

cpu在不同模式运行的区别

  • CPU 运行在系统态时,能够执行所有的系统指令,如开中断、关中断指令等,在寻址方式上采用实地址模式。

  • CPU运行在用户态时,不能执行特权指令,如果所执行的应用程序代码中包含特权指令,CPU 运行时将会出错,此外 CPU在解析程序代码的指令地址和数据地址时采用虚地址方式。

    换言之,运行在不同 CPU模式下的程序代码特性也存在不同,一段拟在用户态运行的代码,将其在系统态运行会出错。

    内核模块的代码在加载到系统内核后,将在CPU 的系统态下执行。

  • 源程序的角度来看,系统态下的程序和用户态下的程序并没有本质的区别,只不过编译器会根据程序员的不同要求,生成适合系统态运行的目标代码,或生成适合在用户态运行的目标代码。

  • 编制好内核模块对应的源程序后,在将源程序编译成目标代码模块 .o(或.ko)文件时,要告知编译器需要编译的是内核模块的代码。具体方式:gcccc选项中添加D_MODULE,这样就能生成适合在系统态运行的Linux内核模块。

4.调试和信息输出

内核调试工具:KDB

KDB以内核源程序补丁的形式存在,通过修改内核源程序将调试器的源代码嵌入到内核中,从而提供方便的调试手段。

因此,使用KDB进行调试,需要重新编译内核,使编译后的内核中包含KDB的调试器代码

其他调试方法:添加一些信息输出语句

linux基本内核中,定义了一个格式化输出的全局函数printk(),将获得的进程信息输出到控制台或日志文件中,该函数在头文件include/linux/printk.h中声明。

Linux内核级安全开发基础

1.5 应用程序和内核模块的信息交互方式

  • 应用程序代码运行在用户态,用户态CPU工作在保护地址模式
  • 内核模块代码运行在系统态,系统态CPU工作在实地址模式

内核模块代码和应用程序代码分别运行在不同的地址空间上,二者不能相互访问和自由地传递数据

内核模块和程序之间的三种数据交换技术

  1. Netlink机制
  2. 创建设备文件
  3. 添加系统调用
  4. 创建proc文件结点
1.Netlink机制

Linux提供一种基于套接字接口的通信机制,即Netlink机制

在内核模块和上层应用程序之间分别建立Netlink协议类型的套接字,经过套接字初始化后,应用程序和内核模块就可以使用这对套接字进行数据传递

优点:

  1. 全双工异步通信,在内核实现Netlink接受队列,即可实现无阻塞的消息通信
  2. 具有“组播”功能,Netlink消息可发送到一个Netlink组地址,所有设定该组地址的进程都能收到该消息
  3. 在内核模块中添加一个Netlink套接字只需进行少量修改

由于这些优点,Netlink机制逐渐成为Linux系统中一种标准的通信机制,例如多种基于Linux开发的相关应用,如路由器、防火墙、IPSec等都采用Netlink机制实现内核和应用程序的通信

Netlink机制中,也有Netlink协议簇的概念,支持范围:031(016:Linux自身或知名应用+17~31供用户定义使用

Netlink机制的使用跟普通的套接字没有本质差别

  1. 创建套接字
  2. 在该套接字上完成数据的发送和接收

只是Netlink套接字通信时消息数据的构造相对复杂

2.创建设备文件

Linux为了屏蔽硬件细节,引入了设备文件这种方式,通过相应的设备驱动将具体设备抽象为设备文件(但网络设备抽象为接口),给应用程序提供一个统一的编程接口

Linux系统中设备的基本类型有:

  1. 字符设备:PC上的串口和键盘
  2. 块设备:硬盘和软盘
  3. 网络设备

Linux系统为每个设备分配了一个主设备号和一个次设备号:

  • 主设备号:标识设备对应的驱动程序
  • 次设备号:标识具体设备的实例

设备文件作为设备访问的入口点,应用程序利用该入口点就能像操作普通文件一样来操作设备

设备驱动程序实质是一组完成不同任务的函数集合(定义在file_operations结构体中,包含常见文件I/O函数的入口地址)

应用程序需要对设备进行操作时:访问设备对应的设备文件结点,内核将调用该设备的相关处理函数,就可以像读写文件一样从设备接收输入和将输出送到设备

实现内核模块和上层应用程序的通信:创建一个虚拟设备,通过驱动中的函数实现内核模块和应用程序之间的数据交互

编写一个字符设备驱动:

  1. 实现file_operations结构体中的需要用到的操作函数,比如write():通过应用程序配置的信息传递给内核模块,以实现相应的资源访问控制

  2. 实现初始化,以在内核中注册该设备

    设备驱动是以一个独立的内核模块形式存在的,在包含设备驱动的内核模块加载时。调用设备初始化函数完成设备注册:将驱动程序的file_operations和主设备号一起向内核进行注册

    字符设备的注册函数:

    1
    2
    3
    4
    5
    int register_chrdev(
    unsigned int major, //申请的主设备号,0:由系统动态分配
    const char name, //设备名
    struct file_operations fops //file_operations结构体
    )

    在包含设备驱动的内核模块卸载时,调用设备的卸载函数完成设备注销

    字符设备的卸载函数:

    1
    2
    3
    4
    5
    int unregister_chrdev(
    unsigned int major, //要注销的设备号
    const char name, //设备名
    struct file_operations fops //file_operations结构体
    )
3.添加系统调用

在使用系统调用提供的各类服务时,应用程序要和Linux内核交互数据,系统调用中的参数就是用来在应用程序和Linux内核间完成数据交互的。

所以,新添加一个带参数的系统调用就可以完成应用程序和内核模块之间的信息传递

实现新的系统调用的步骤:

  1. 为新添加的系统调用预先分配一个系统调用号
  2. 实现相应的系统调用函数
  3. 将该函数的入口地址写到系统调用入口地址表的对应表项中

新添加的系统调用分为:

  1. 静态实现:直接修改Linux操作系统源码

    1. 分配新的系统调用号
    2. 实现新的系统调用函数
    3. 修改系统调用入口地址表的源代码:在系统调用号对应表项写入新函数的入口地址

    因为修改了内核源码,需要对内核源码重新编译

  2. 动态实现:用内核模块的形式进行系统调用的扩展

    1. 分配新的系统调用号
    2. 实现新的系统调用函数
    3. 在模块初始化函数中,将新函数的入口地址写道系统调用表的对应表项

​ 该方法无需重启操作系统,也无需重新编译操作系统源代码

Linux内核级安全开发基础

1.4 Linux系统调用的实现

1.系统调用入口地址表

每个系统调用在Linux内核中都有一个对应的处理函数——>完成该系统调用对应的服务功能

系统调用入口地址表

  • 实质:地址数组,下标:系统调用号,元素内容:对应系统调用的处理函数入口地址
2.中断机制和系统调用实现

系统调用接口实现的核心问题:CPU如何进行运行模式的切换,即CPU在用户态执行应用程序过程中,遇到系统调用请求,如何转向内核态执行操作系统中该系统调用对应的处理函数

中断机制

  • CPU收到中断信号,就会暂停当前的任务,自动转换到内核态进行相应的中断处理

  • 中断向量表:

    • 为了方便管理不同的中断,按照中断源不同将中断进行细分并编号,每个编号的中断对应相应的处理函数
    • 下标=编号,元素=中断处理函数的入口地址
  • 中断按处理方式不同可分为三类

    1. 硬件中断:严格意义上的中断,是外部设备与CPU进行通信的主要形式,也是CPU和外部设备能够并行工作的技术保证

    2. CPU执行异常:比如说CPU在执行指令过程中发现除以0的情况,就会发出异常信号,进行相应的异常处理

    3. 自陷trap:特殊的中断形式,为了让CPU在执行过程中能够主动切换到内核态执行,以进行相应的处理

      硬件中断和后面两种形式的中断在具体处理上有明显差别:

      • 硬件中断:中断处理完成,CPU会重新执行被中断的指令
      • 异常和自陷:中断处理完成,CPU直接执行下一条指令
  • 系统调用是借助于系统自陷实现的,通过执行相应的机器代码指令(自陷指令)产生中断信号,CPU自动从用户态切换到系统态进行处理。处理完成后,继续执行系统调用后面的指令

3.Linux系统调用的实现过程
  • Linux用来实现系统自陷的实际机器指令是 int x80,执行效果:激发一个x80号中断——>对应中断向量号128==是Linux系统为系统调用接口分配的中断编号,CPU执行该指令时,通过向量号将控制权转移给内核。

  • 程序员无需插入该汇编指令,只要像调用库函数一样调用系统调用即可。编译器在将源程序编译成可执行文件时,会生成相应的机器指令代码。

  • 具体过程:

    1. 编译出的目标程序在执行到intx 80位置时,CPU切换到系统态运行,将控制权转移给操作系统内核(即开始执行操作系统内核的代码)

    2. 内核中的代码查找中断向量表中x80号对应的中断处理函数,即系统调用总入口函数

      该函数大致任务:

      1. 保存寄存器等各种运行现场
      2. 从相应寄存器中获取系统调用号
      3. 根据系统调用号,从系统调用入口函数地址表获得处理函数的入口地址
      4. 调用处理函数,将结果保存在相应寄存器
      5. 完成系统调用出,从内核态返回,将结果反馈给用户进程,用户进程继续执行下一条指令

Linux内核级安全开发基础

1.3 Linux系统调用概述

1.系统调用与系统安全

当代计算机体系中

  • 操作系统内核运行在系统态
  • 应用程序运行在内核态——权限受到限制:不能执行特权指令,不能直接对硬件进行操作

===>有利于保证操作系统的安全性

——>带来问题:应用程序为了应用任务需要执行特权指令或者访问硬件,例:访问磁盘来保存自己的数据

解决思路:操作系统为应用程序提供相应的服务,应用程序间接执行特权指令或者访问硬件

  1. 操作系统在提供服务之前进行安全控制检查
  2. 检查通过,提供相应的特权操作或硬件访问操作

这是操作系统的重要任务之一:为应用程序的运行提供各种操作系统服务

——应用程序要访问硬件或者执行某特权操作,只需要调用相应的操作系统服务,这就是系统调用过程

——每一种类型的服务被称为一个系统调用

2.系统调用的服务功能

操作系统的服务是以系统调用接口形式存在的

系统调用是应用程序和操作系统内核之间的功能接口

  • 目的:比较方便使用操作系统提供的有关设备管理、输入/输出系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不用了解系统内核代码的内部结构和有关硬件细节
  • 编程中用到的很多函数,如open()write()read(),都跟具体的系统调用相对应

Python中集成了专用于处理csv文件的库,名为:csv
csv 库中有4个常用的对象:

  • csv.reader:以列表的形式返回读取的数据。
  • csv.writer:以列表的形式写入数据。
  • csv.DictReader:以字典的形式返回读取的数据。
  • csv.DictWriter:以字典的形式写入数据。

读取csv文件

使用csv.reader读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 导入 csv 库
import csv

# 以读方式打开文件
with open("data.csv", mode="r", encoding="utf-8-sig") as f:

# 基于打开的文件,创建csv.reader实例
reader = csv.reader(f)

# 获取第一行的header
# header[0] = "设备编号"
# header[1] = "温度"
# header[2] = "湿度"
# header[3] = "转速"
header = next(reader)

# 逐行获取数据,并输出
for row in reader:
print("{}{}: {}={}, {}={}, {}={}".format(header[0], row[0],
header[1], row[1],
header[2], row[2],
header[3], row[3]))

使用csv.DictReader读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 导入 csv 库
import csv

# 打开文件
with open("data.csv", encoding="utf-8-sig", mode="r") as f:

# 基于打开的文件,创建csv.DictReader实例
reader = csv.DictReader(f)

# 输出信息
for row in reader:
print("设备编号{}: 温度={}, 湿度={}, 转速={}".format(row["设备编号"],
row["温度"],
row["湿度"],
row["转速"]))

写入csv文件

使用csv.writer写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 导入 csv 库
import csv

# 创建列表,保存header内容
header_list = ["设备编号", "温度", "湿度", "转速"]

# 创建列表,保存数据
data_list = [
[0, 31, 20, 1000],
[1, 30, 22, 998],
[2, 32, 33, 1005]
]

# 以写方式打开文件。注意添加 newline="",否则会在两行数据之间都插入一行空白。
with open("new_data.csv", mode="w", encoding="utf-8-sig", newline="") as f:

# 基于打开的文件,创建 csv.writer 实例
writer = csv.writer(f)

# 写入 header。
# writerow() 一次只能写入一行。
writer.writerow(header_list)

# 写入数据。
# writerows() 一次写入多行。
writer.writerows(data_list)

使用csv.DictWriter写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 导入 csv 库
import csv

# 创建 header 列表
header_list = ["设备编号", "温度", "湿度", "转速"]

# 创建数据列表,列表的每个元素都是字典
data_list = [
{"设备编号": "0", "温度": 31, "湿度": 20, "转速": 1000},
{"设备编号": "1", "温度": 30, "湿度": 22, "转速": 998},
{"设备编号": "2", "温度": 32, "湿度": 23, "转速": 1005},
]

# 以写方式打开文件。注意添加 newline="",否则会在两行数据之间都插入一行空白。
with open("new_data.csv", mode="w", encoding="utf-8-sig", newline="") as f:

# 基于打开的文件,创建 csv.DictWriter 实例,将 header 列表作为参数传入。
writer = csv.DictWriter(f, header_list)

# 写入 header
writer.writeheader()

# 写入数据
writer.writerows(data_list)

关于写入,需要注意:

  1. 在打开文件时,需要添加newline = “”。否则,会在每2行有效内容之间添加一行空白。
  2. 如果要保存的内容有中文,而且之后需要用Excel打开文件,那么需要选用utf-8-sig编码。如果使用utf-8编码,会导致使用Excel查看文件时中文乱码。