A for algorithm
这周的算法练习是Roman-to-integer。比较简单,一个字符变量读取当下字符串位置的字符,并用switch语句将对应的整数值装到cur_num(当前整数)中,同时设一个整数值prev_num保存上一个位置的整数值,这个值初始化为0。if语句判断当前数值是否小于上一个数值,是的话就说明遇到了IV,IX,XC之类的特殊情况,将原先累加的result减掉两倍的prev_num,继续往后,直到读完整个字符串。代码如下:
int romanToInt(char* s) {
char c = *s;
int result = 0;
int prev_num = 0;
int cur_num = 0;
while(c != NULL && c != EOF)
{
switch(c){
case 'M':cur_num = 1000;break;
case 'D':cur_num = 500; break;
case 'C':cur_num = 100; break;
case 'L':cur_num = 50; break;
case 'X':cur_num = 10; break;
case 'V':cur_num = 5; break;
case 'I':cur_num = 1; break;
default: return -1;
}
if(prev_num == 0 || prev_num >= cur_num)
{
result += cur_num;
} else if (prev_num < cur_num)
{
result += (cur_num - 2*prev_num);
}
prev_num = cur_num;
s++;
c=*s;
}
return result;
}
R for review
继续在啃CSAPP。目前第三章基本看完了,计划在4月15日之前搞完后面的习题。我从3.7节过程开始看的,书里在介绍函数调用时寄存器的使用和栈的变化。不过这一部分在第13节谈到x86-64时,还是有些变化的。比如x86-64不再使用帧指针,而且栈指针几乎固定(除非是使用push和pop)来改变栈指针,如无必要,不再对栈指针加加减减。而且考虑到寄存器的数量和大小均翻倍了,因此对栈的依赖变小了,有些函数调用无需建立栈,直接在寄存器时调用即可。而且x86-64规定的red zone,栈指针下方(地址更低)的128个字节也可以直接使用,以栈指针为基准,无需依赖栈,读写数据也更方便了。
中间有一部分讲数组结构体联合这几类非基本数据类型编译器的空间分配,均是分配一块连续的地址空间,相对帧指针或栈指针。针对数据类型,有基本的地址对齐要求(在后面的window机器上要求更严),不过似乎x86没有明确要求地址一定要对齐,但是对齐是推荐的做法,因为有利于提高存储器效率(这还需要看到第6章才能详细了解)。
3.12节谈到缓冲区溢出。由于C不提供边界检查,在实际输入的时候很有可能超过栈上分配的缓冲区大小,覆盖了被调用者保存的寄存器,帧指针,甚至是返回地址。恶意软件可以利用这一点,覆盖返回地址使程序返回到恶意软件的部分开始执行。书里提到了三种方法用来对抗缓冲区溢出。
- 栈随机化,每次运行程序的时候随机分配栈的地址,比如linux系统,大致能随机生成2的13次方个随机地址(连续),但是入侵者仍有可能使用no-op sled摸索到实际栈的位置开始攻击,如果no-op sled长度为128字节(2的7次方),入侵者尝试64次(2的6次方)就能成功了,不过如果是64位机器,随机化地址范围可以变更大,攻击难度也就变大了。
- 栈破坏检测,在缓冲区和栈状态(保存了寄存器值返回地址等)之间插入哨兵值,又叫金丝雀值,这个值也是来自系统存储任意某处的值。如果返回前发现这个值产生了变化,程序就异常中止。
- 限定可执行代码区域,x86原先将读写执行并做一起,现在区分对待,某些区域没有执行权限,因此避免恶意代码运行。
T for tip
做题的时候搜到一个bash小功能,十进制转十六进制,使用printf '%x\n' <待转换的数> 即可。同理'%d'和'%o'也可以产生十进制和八进制的数。不过printf好像没有二进制表示。
S for summary
继续在看计算机体系结构的书,正好目前看的1.3节和caspp对x86架构的很多描述刚好对上。作者表示体系结构不应该仅局限于指令集架构,但也大致介绍了一下指令集架构。x86目前是16个通用寄存器(其中8个应该是整数类型寄存器%eax这一类的,此外还有程序计数器%eip等),和16个浮点寄存器。两种指令集模型为,第一类似x86,可以直接从存储器中读取数据,第二种类似ARM,需要load-store(毕竟ARM有30多个寄存器)。
取址的对齐要求和索引地址的方式,书里所提到的indrect, indexed,based with scaled index正是caspp里提到的 (%eax),(%eax,%edx),(%eax,%edx,4)这样的类型。从寄存器eax里读取地址,再从地址里读取数据;从eax里读取基地址,再加上edx中的偏移量得到实际地址,从实际地址获取数据;从eax里读取基地址,再从edx里读取偏移数字,乘以数据单位大小(如整型数据,4字节大小),获得真实的字节偏移量加到基地址上,再读取数据(正是数组索引的办法)。
操作数的大小,各类ISA均支持8位,16位,32位,64位,x86还支持扩展浮点数80位(实际保存需要12个字节)。操作的大类也大致分为,数据传送、算术运算、控制、还有浮点运算。
有关函数过程,有提到x86架构返回地址会入栈,存放在栈中。但是ARM和MIPS会放在寄存器中。
还有就是有关指令的机器码长度。x86的不同指令对应的机器码长度不等,越常用的越短。尽管这样转译的过程可能更为复杂,但是有利于压缩程序大小。因此ARM也推出了Thumb和Thumb-2的指令格式,将32位指令压缩为16位。
撇开指令集架构的不同,计算机还在硬件的性能上有所差异。同样的指令集架构,搭载的硬件不同,性能当然也会不一样。不过这算一个common sense,没有最好的,只有最合适的。基于人们对于不同设备的功能期望和成本限制,手持数字设备侧重反应速度,节能和多媒体效果。服务器设备则要求稳定和规模。
技术上来看,从CPU到多核处理器,主存,磁盘等设备的造价越来越便宜,闪存也使得手持数字设备变得普及。尽管下降的趋势逐渐放缓,但还是可以期待一下以后技术累积到一定程度,又会有什么新的突破呢?
目前ARTS也是第二周了,和在群里的很多小伙伴不同,我不是以写代码为主业,但是有过一些经验,也干过这样的工作(尽管因为工作不愉快直接辞掉了),后来也面试过相关的岗位,因为基础薄弱被筛掉了。现在保持学习的状态不排除功利的想法(比如跳槽重新写代码),但是更多的是希望自己能够保持好的学习习惯,ARTS计划开始是第二周,但是我开始看CSAPP是从月初,虽然时间不长,但是几乎每天都能保持一到两小时的时间看书,如此做下来,对未来的焦虑感不再困扰到我,自己也更加笃定。专注能够产生幸福感,学习也是一项修炼。各位小伙伴加油。