8086 CPU一般是这样:CS寄存器初始化为0xF000,IP寄存器初始化为0xFFF0,所以按照CPU实模式地址计算法则,CPU执行的第一条指令地址是CS*10h+IP,即0xFFFF0处
对于80386以上的CPU:
第一点,80386及其以上的现代CPU(以下简称CPU)加电Reset之后并不是直接进入实模式;
第二点,CPU在合成地址的时候不区分实模式和保护模式。
CPU进入保护模式的方法是CR0寄存器的PE Bit置为1。而在CPU刚加电的时候,CR0寄存器的PE位实际上没有置1,但是此时也不是实模式,Intel并没有给给出表示此时CPU状态的术语名词。自从80386以来,因为增加了保护模式的缘故,CS等段寄存器不再是简简单单的段寄存器了,而是一个包含了段选择器(segment selector)、段基址(segment base),以及段限制(segment limit)的一组复杂寄存器。显然段基址决定着内存段的基地址。不过需要说明的是作为程序员只能操作CS寄存器中的“段选择器”这16位的大小,其它的区域作为隐藏区域对程序员不可见,我们无法访问。
合成地址时,假设段选择器(我们能访问的那16位)装入了0xF000,那么CPU会先将F000 * 10h也就是F0000h装入段基址里。之后需要合成地址的时候不考虑别的,而是直接从之前合成好的段基址里读出基地址F0000h加上IP寄存器里的偏移生成地址。如果CS寄存器的值不发生改变,段基址部分就不会发生改变。所以我们说,CPU在合成地址的时候不区分实模式和保护模式,CPU只是机械的从隐藏区域读出来段基址和IP寄存器的数值相加。
Intel这样做是为了效率,虽然实模式的地址计算很快,但是保护模式计算一个地址还要有去内存中寻找段描述符等工作,这会大大影响CPU的效率
放弃,无法理解。。。。。
原文:http://0xffffff.org/2013/03/14/15-x86-boot/
32硬件平台下,进程拥有4GB的线性地址空间(并不是物理地址空间)
启动分段机制,未启动分页机制:逻辑地址--> (分段地址转换) -->线性地址==物理地址
启动分段和分页机制:逻辑地址--> (分段地址转换) -->线性地址-->(分页地址转换) -->物理地址
逻辑地址空间:从应用程序的角度看,逻辑地址空间就是应用程序员编程所用到的地址空间,比如下面的程序片段: int val=100; int * point=&val;
其中指针变量point中存储的即是一个逻辑地址。在基于80386的计算机系统中,逻辑地址由16位的段寄存器(也称段选择子,段选择子)和32位的偏移量构成。
进程线性地址空间:
内存高地址区域是被操作系统内核所占据的,Linux操作系统占据了高地址区域的1GB内存(Windows系统默认保留2GB给操作系统,但是可以配置为保留1GB)。如果我们想知道一个进程具体的内存空间布局的话,可以去/proc目录找以进程的pid所命名的目录下一个叫maps的文件,使用cat命令查看即可(需要root权限)。
32位Linux系统中,代码段从线性地址0x08048000处开始的。数据段一般是在下一个4KB(分页机制默认选择4KB一个内存页)对齐的地址处开始。运行时堆是在数据段之后又一个4KB对齐处开始的,并通过malloc()函数调用向上增长(Linux下的malloc()一般依靠调用brk()或者mmap()系统调用实现)。再接着跳过动态链接库的区域就是进程的运行时栈了,需要注意的是栈是由高地址向着低地址增长的(看图中esp寄存器内容增长方向)。栈空间再往上就是操作系统保留区域了,用于驻留内核的代码和数据。即就是在一个进程的眼里,只有它和操作系统在一起。
我们具体来看看代码段,以C语言为例,程序代码段的入口_start地址处的启动代码(startup code)是在目标文件ctr1.o(属于C运行时库的部分)中定义的,对于特定平台上的C程序都一样。其执行流程如下:
0x080480c0 <_start>: 调用 __libc_init_first 函数 调用 _init 函数 调用 atexit 函数 调用 main 函数 调用 _exit 函数
例子:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int a = 1; // 栈区 static int b = 2; // 全局静态区(读/写段) const int c = 3; // 只读段 char *str1 = "Hello"; // str1指针在栈区,"hello"字符串在只读段 char str2[] = "world"; // 只在栈区(字符串) char *str3 = (char *)malloc(20); // str3变量在栈区,指针指向堆区 return EXIT_SUCCESS; }
注释中我们看到了各个元素所在内存段的位置。而编译好的main函数代码本身是存在于代码区的(一般代码段也是只读段)。我们这个程序运行后如果是动态链接的C语言运行时库的话,动态库会存在图示的动态库映射区。无论使用C语言运行时库的程序无论有多少,运行时库的代码在内存里只会有一份,对于不同的程序,进行地址映射即可。
栈也经常被叫做栈帧(Stack Frame)或者活动记录(Activate Record)。栈里通常存储以下内容:
1.函数的临时变量;
2.函数的返回地址和参数;
3.函数调用过程中保存的上下文。
在i386中,使用esp和ebp寄存器划定范围。esp寄存器始终指向栈顶,随着压栈和出栈操作而改变值。ebp寄存器随着调用过程,暂时的指向一个固定的栈位置,便于寻址操作的进行。
线程中的地址空间:
从Linux内核的角度来说,它并没有线程这个概念。在内核中,线程看起来就像是一个普通的进程。
一个进程所创建的所有线程没有创建新的地址空间,而是共享着进程所拥有的4G的线性空间,文件系统资源、文件描述符、信号处理程序以及被阻断的信号等内容。不过即便是共享地址空间,但是每个线程还是有自己的私有数据的,比如线程的运行时栈。
#include <unistd.h> #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> void *thread_func(void *args) { printf("tid: %u pid: %u thread id: %un", getpid(), syscall(224), pthread_self()); while(1) { sleep(10); } } int main(int argc, char *argv[]) { pthread_t thread; int count = 0; while (pthread_create(&thread, NULL, thread_func, NULL) == 0) { sleep(1); count++; } perror("Create Error:"); printf("Max Count:%dn", count); return EXIT_SUCCESS; }
要动态链接lpthread
#include <unistd.h> #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> int *p_num; void *thread_1(void *args) { int test_num = 1; printf("test_num: %dn", test_num); p_num = &test_num; sleep(2); printf("test_num: %dn", test_num); } void *thread_2(void *args) { sleep(1); if (p_num != NULL) { *p_num = 2; } } int main(int argc, char *argv[]) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_1, NULL); pthread_create(&thread2, NULL, thread_2, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); return EXIT_SUCCESS; }
thread1修改了thread2内容,所以线程内存空间是共享的
Done!!!
引用:
http://0xffffff.org/2013/05/23/18-linux-process-address-space/
http://0xffffff.org/2013/08/02/20-linux-thread-address-space/