1。大端小端的一个测试程序
在之前一篇文章中有提到,下面是写的一个测试程序,可以用来查看一个机器是大端还是小端表示
1 #include<stdio.h> 2 typedef unsigned char * bytePointer; 3 void show_bytes(bytePointer start, int size){ 4 printf("start address : %p\nsize : %d\n", start, size); 5 for (int i = 0; i < size; i++){ 6 printf("address : %p, value: %.2x\n", start+i, start[i]); 7 } 8 printf("\n"); 9 } 10 void show_int(int n){ 11 printf("int n = (0x)%x\n", n); 12 show_bytes((bytePointer)&n, sizeof(int)); 13 } 14 void show_pointer(void * p){ 15 printf("pointer value : %p\n", p); 16 show_bytes((bytePointer)&p, sizeof(void *)); 17 } 18 /*void show_float(float f){ 19 printf("float value : %f\n", f); 20 show_bytes((bytePointer)&f, sizeof(float)); 21 }*/ 22 int main(){ 23 int n = 0x11223344; 24 show_int(n); 25 show_pointer(&n); 26 // show_float(float(n)); 27 show_int(n); 28 return 0; 29 }
先看第26行,float(n)这样的语句我测试发现c是不支持的,c99也不行,这是C++的特性,要用g++才能编译过,但是看12,16行对show_bytes的调用,c支持把一个指针类型转换为另一个指针类型,这可能是c从最早就开始支持的东西吧。
运行结果如下
int n = (0x)11223344 start address : 0x7fff4af6b5fc // show_int中的n的地址 size : 4 address : 0x7fff4af6b5fc, value: 44 address : 0x7fff4af6b5fd, value: 33 address : 0x7fff4af6b5fe, value: 22 address : 0x7fff4af6b5ff, value: 11 pointer value : 0x7fff4af6b61c // main中的n的地址 start address : 0x7fff4af6b5f8 size : 8 address : 0x7fff4af6b5f8, value: 1c address : 0x7fff4af6b5f9, value: b6 address : 0x7fff4af6b5fa, value: f6 address : 0x7fff4af6b5fb, value: 4a address : 0x7fff4af6b5fc, value: ff address : 0x7fff4af6b5fd, value: 7f address : 0x7fff4af6b5fe, value: 00 address : 0x7fff4af6b5ff, value: 00 int n = (0x)11223344 start address : 0x7fff4af6b5fc size : 4 address : 0x7fff4af6b5fc, value: 44 address : 0x7fff4af6b5fd, value: 33 address : 0x7fff4af6b5fe, value: 22 address : 0x7fff4af6b5ff, value: 11
可以看到,我的机子是小端表示法的,低位地址放的是最低有效位的数(在第一个测试数据中,0x11223344中44是最低有效位)。 换一个角度从栈的角度来看,栈是从高地址向低地址增长的,这里几个变量的地址都是从 0x7fff4af6b5ff 开始向下占用的空间,int是4字节,void* 是8字节,实际上是在我本机上所有的指针都是8字节。这里站在栈空间的角度想一下,我先不把对应的GAS代码贴出来了,总之每个过程调用都分配了16字节的局部空间,而n和&n(参数或局部变量)都放在这16字节的最高地址,根据代码,上面的输出结果中的pointer value实际上是main函数中那个n的地址,即main中&n的值,而第一段和第三段中的start address是show_int中那个参数即局部变量的地址,注意地址都是最低位字结的地址。可以看到这两地址值相差了 0x20, 即32字节,怎么解释这32字节呢,很简单,本身main这个frame为局部变量分配了16字节,call show_int 时会压入返回址这是8字节,然后在show_int的frame中,push %ebp 即压入main的frame base pointer这会占8字节,这就是32字节了,show_int中的n就存放在main的%ebp下面。
2。栈如何完成从一个过程到另一个过程的跳转
之前的两篇随笔中已经有大量的过程的例子了,任何一个过程开始时都是 push %ebp ; mov %esp %ebp; 结束时都是 leave; ret ; 从一个过程到另一个过程的跳转即调用就是call, call在上面这段话中已经很清晰了,会压入调用者下一条指令的地址。leave指令其实相当于下面两条指令,在我的机器上
1 movq %rbp %rsp 2 popq %rbp
把这两个指令和过程开始时的两系指令形象的想象一下就好了, 然后ret是从栈中弹出调用者下一条该执行的指令并跳转。 栈是用来完成数据的传递和返回的,跳转指令用来完成流程跳转,所以如果不是函数调用一般的跳转都是在本函数内。
3。过程调用数据的转移
一个过程调用包括将数据和控制从代码的一个部份转移到另一个部份,数据的转移以参数和返回值的形式,以及在进入新过程的时候空间的分配和退出时的释放,这个在前面的例子中已经提得比较多了。这里谈一谈数据的转移,在书中3.7.2有提到寄存器的使用惯例,书中提到的是IA32使用的是这统一的寄存器惯例,除了%ebp, %esp外其作6个寄存器被分为了调用者保存(caller save)和被调用者保存(callee save),这里明白这样的概念和方式就好了, caller save的寄存器被调用者可以随意使用,经常%eax就用来存放返回值,这在前面的很多例子中可以看到,caller要确保这些寄存器中不存放局部变量,即都会放在栈中。而callee save的寄存器就必须由被调用者来保证这些寄存器要么不使用,使用的话则必须退出时恢复,像%esi, %edi就是,它们被用来做承载参数的传递,参数基本上都是caller的局部变量,若参数很多的话,寄存器不够用了,根据我自己测试的例子,会在被调用者中直接通过%ebp去调用者的frame中引用局部变量。
所有的局部变量都会在栈中保存。
还是继续看一个例子,这个例子是书中的,但是在我的机器上得到的GAS和书中的有一些差别
1 int swap_add(int* xp, int *yp){ 2 int x = *xp; 3 int y = *yp; 4 *xp = y; 5 *yp = x; 6 return x+y; 7 } 8 int caller(){ 9 int arg1 = 222; 10 int arg2 = 333; 11 int sum = swap_add(&arg1, &arg2); 12 int diff = arg1 - arg2; 13 return sum*diff; 14 }
对应的GAS代码
1 swap_add: 2 pushq %rbp 3 movq %rsp, %rbp 4 movq %rdi, -24(%rbp) # xp (局部变量) 5 movq %rsi, -32(%rbp) # yp 6 movq -24(%rbp), %rax # int x = *xp 7 movl (%rax), %eax 8 movl %eax, -8(%rbp) # x 9 movq -32(%rbp), %rax # int y = *yp 10 movl (%rax), %eax 11 movl %eax, -4(%rbp) # y 12 movq -24(%rbp), %rax 13 movl -4(%rbp), %edx 14 movl %edx, (%rax) # x = *yp 15 movq -32(%rbp), %rax 16 movl -8(%rbp), %edx 17 movl %edx, (%rax) # y = *xp (反复使用%ax,%dx) 18 movl -4(%rbp), %eax 19 movl -8(%rbp), %edx 20 addl %edx, %eax 21 popq %rbp 22 ret 23 caller: 24 pushq %rbp 25 movq %rsp, %rbp 26 subq $16, %rsp 27 movl $222, -16(%rbp) 28 movl $333, -12(%rbp) 29 leaq -12(%rbp), %rdx 30 leaq -16(%rbp), %rax 31 movq %rdx, %rsi 32 movq %rax, %rdi 33 call swap_add 34 movl %eax, -8(%rbp) 35 movl -16(%rbp), %edx 36 movl -12(%rbp), %eax 37 movl %edx, %ecx 38 subl %eax, %ecx 39 movl %ecx, %eax 40 movl %eax, -4(%rbp) 41 movl -8(%rbp), %eax 42 imull -4(%rbp), %eax 43 leave 44 ret
在caller当中,arg2,arg2作为局部变量,都在栈中有空间,但&arg1,&arg2是没有的,不知道是算不算临时变量,它们的值都放在%rdx,%rax中,注意31,32行,它们被mov到了%rsi, %rdi中再call swap_add, 可以看到%rdx, %rax是调用者保存寄存器,%rsi,%rdi是被调用者保存寄存器,参数的传递使用了%rsi,%rdi。
另外还有一个现象,在caller 26行为这个过程分配了16字节的局部空间,而在swap_add中并没有,我测试的时候发现若在swap_add中再调用另一个过程,则也会分配局部空间,也就是栈中最下层的frame是不会分配局部空间的。所以这也是为什么是底层的frame退出是没有用leave指令,用的是pop, 如21行
swap_add和caller的返回值都放到了%eax中。 另外在swap_add中,在把%rsi,%rdi中存放的参数值放到栈中后就没有使用这两个寄存器了,而使用的是%eax, %edx了, 同样的事情也在caller中发生,在本过程中随意使用%eax, %edx , %ecx,因为这三个都是caller save的,不用担心会污染丢失数据。
也就是所有的局部变量都会在栈中保存,若调用者把一个数放到被调用者寄存器中如%edi, 则这个数之后再被caller使用时就可以直接使用,因为callee负责保全它,若是放到caller save register如%edx,则调用之后再使用必须去栈中取了。细看一下swap_add可以体会到编译器,机器翻译一些特点
4。一个书中的习题 3.14(computer system , a programmer's perspective)
1 call next 2 next: 3 popq %rax
仔细体会一下,这并不是一个真正的过程调用,没有一个完整的过程调用的很多东西。这段代码的作用实际上是把 3行,popq %rax指令的地址放到%rax中,这是IA32中将IP(instruction pointer)中的值放到整数寄存器的唯一方法。 call next压进栈的地址就是popq %rax的地址