程序员的自我修养
0x00 口胡
最近也算是闲下来了,把最近一直想总结的exp给总结完后,就想着找点事干。。。
这两天就去看各个大大的blog,理解了哈希长度扩展攻击,看了一些PHP黑魔法,强势补了下分区的姿势,SSRF啥的。。。但也就是浏览浏览,根本沉不下来心,感觉自己太浮躁了,然后又去看一些自己错过的一些ctf的write up,想着自己去把一些想做的题去重新做一下,但根本找不到真正打比赛时的激情,感觉就像是在咀嚼剩菜,不行,提不起来劲。。。
总不能浪费时间吧,安卓和硬件的话想放假回来搞,现在的心情真心不适合去马上开始一块新的领域,主要是自己真的不想搭环境,想赖着两个队友,他们把环境都搭好,自己直接用~
问了下两个队友,他们也都是在各种总结,所以自己想着还是把近几个月一直在看的这本书给总结下,算是给临近放假的这段时间+寒假找点事干,也让自己静静,沉淀沉淀。。。
这个blog就准备当做自己的一个总结用的东西吧,写的也都是给自己看的一些基础的东西,并不是那么注重质量,就是当做自己忘了某些东西时,有个可以查的地方吧,所以基本都是一些理论的东西,实战很少。
等自己牛逼了,再重新开个blog,介绍各种奇淫技巧也不迟~
0x0.5 关于本系列的口胡
还是会写成一个系列吧。。。
会按照本书的脉络,总结自己认为重要的东西,会尽量注重体系化和结构化,对于一些自己认为太干的货,我还是会加入自己的口水理解,方便自己消化。
跟操作系统会有点联系吧,就是一些较底层的理论姿势,发现自己也是喜欢这些东西。。。
口胡就到这吧,开始漫漫总结之旅。。。
没找到电子版,pdf找的也都是扫描版,所以能手打的就手打了,纯当练习自己的打字速度+加深记忆了,但是自己真的懒,所以截图大法好。。。
0x01 线程的笼统介绍
线程,有时被称为轻量级进程。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成,各个线程之间共享程序的内存空间。这里说的寄存器集合并非指真正意义上的物理硬件,一个线程就给一套与它相匹配的寄存器,这。。。这里更偏向于软件,是一种数据结构的概念,还记得操系中的TSS吗,类似于那个概念吧,就是说当线程真正竞争到CPU资源时,他才对这些物理资源实现真正意义上的占有。
相对于多进程应用,多线程在数据共享方面效率要高的多,因为进程间通信限制很多,这些限制是为了防止各种杂七杂八的情况出现。线程的访问很自由,他可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果他知道其他线程的堆栈地址)。但线程也是有隐私的,局部变量,函数的参数,线程局部存储(TLS),这些数据是线程私有的。
关于线程的调度问题,具体参考进程调度,差不多。如果想要提高一个线程的被调度率,可以采用提高线程优先级的方法。在windows中,使用BOOL WINAPI SetThreadPriority(HANDLE hThread,int nPriority);来设置线程的优先级,而linux下与线程相关的操作可以通过pthread库来实现。一般的策略中,IO密集型总是比CPU密集型更容易得到优先级的提升。在优先级调度的环境下,线程的优先级改变一般有三种方式:
- 用户指定优先级
- 根据进入等待状态的频繁程度提升或降低优先级。频繁等待优先级高,意味着一旦就绪系统就会让他尽量在下个时间片运行
- 长时间得不到执行而被提升优先级。
关于fork,exec,clone以及写时复制、竞争与原子操作,同步与互斥啥的就不啰嗦了,操系里已经写得很清楚了
线程安全是一块烫手的山芋,因为即使合理的使用了锁,也不一定能保证线程安全,这是源于落后的编译器技术已经无法满足日益增长的并发需求。很多看似无错的代码在优化和并发的面前又产生了麻烦。如下代码:1
2
3
4
5x=0;
thread1 thread2
lock(); lock();
x++; x++;
unlock(); unlock();
由于有lock和unlock的保护,x++的行为不会被并发所破坏,那么x的值似乎必然是2了。然而,如果编译器为了提高x的访问速度,把x放到了某个寄存器里,那么我们知道不同线程的寄存器是各自独立的。假设thread1先获得锁,则可能会出现下面的情况:
可见在这样的情况下即使正确的加锁,也不能保证线程安全。下面是另一个例子:
此时,如果按照1
2
3
4
5
6r1=y;
切换
y=1;
r2=x;
切换
x=1;
这种顺序执行,那么此时r1=r2=0就完全有可能了。看到这,其实是有点崩溃的,这尼玛考虑的问题也太多了吧,老子本来写的对的程序硬生生跑错,但没办法,已经发展成这样子了。所以,我们来看下补救措施:我们可以使用volatile关键字试图阻止过渡优化。volatile可以做到:
可见volatile确实可以解决一些问题,但是即使volatile能够阻止编译器调整顺序,也无法阻止CPU动态调度换序,呵呵哒。。。
另一个颇为著名的与换序有关的问题来自于singleton模式的double-check。1
2
3
4
5
6
7
8
9
10
11
12volatile T * pInst = 0;
T * GetInstance()
{
if (pInst == NULL)
{
lock();
if (pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
抛开逻辑,这代码乍看没问题,使用了volatile关键字,而lock和unlock防止了多线程竞争导致的麻烦。双重的if在这里另有妙用(防止在lock前进行进程切换而使得代码流程不受我们控制),可以让lock的调用开销降低到最小。
但是,呵呵,这是有问题的,没错,就是CPU的乱序执行。C++里的new其实包含了两个步骤:
- 分配内存
- 调用构造函数
所以pInst = new T包含了三个步骤: - 分配内存
- 在内存的位置上调用构造函数
- 将内存的地址赋值给pInst
1 | #define barrier() __asm()__ volatile ("lwsync") |
由于barrier的存在,对象的构造一定在barrier执行之前完成,因此当pInst被赋值时,对象总是完好的。
首篇就先到这了。。。