C++程序运行时的内存与地址

本文全部内容,以SSD6的Exercise1为示例。

代码如下图。

#include <stdio.h>
#include <stdlib.h>
#include <iostream>

int prologue [] = {
	0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
	0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
	0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
	0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
	0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
	0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
	0x20206F74, 0x74786565, 0x65617276, 0x32727463,
	0x594E2020, 0x206F776F, 0x79727574, 0x4563200A
};

int data [] = {
	0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
      	0x466D203A, 0x65693A72, 0x43646E20, 0x6F54540A,
      	0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
      	0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
      	0x6F786F68, 0x6E696373, 0x6C206765, 0x796C656B,
      	0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
      	0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
      	0x20206F74, 0x74786565, 0x65617276, 0x32727463,
      	0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
      	0x21687467, 0x63002065, 0x6C6C7861, 0x78742078,
      	0x6578206F, 0x72747878, 0x78636178, 0x00783174
};

int epilogue [] = {
	0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
	0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
	0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
	0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
	0x20206F74, 0x74786565, 0x65617276, 0x32727463
};

char message[150];

void usage_and_exit(char * program_name) {
	fprintf(stderr, "USAGE: %s key1 key2 key3 key4\n", program_name);
	exit(1);
}

void process_keys12 (int * key1, int * key2) {
	
	*((int *) (key1 + *key1)) = *key2;
}

void process_keys34 (int * key3, int * key4) {

	*(((int *)&key3) + *key3) += *key4;
}

char * extract_message1(int start, int stride) {
	int i, j, k;
	int done = 0;

	for (i = 0, j = start + 1; ! done; j++) {
		for (k = 1; k < stride; k++, j++, i++) {

			if (*(((char *) data) + j) == '\0') {
				done = 1;
				break;
			}
							 
			message[i] = *(((char *) data) + j);
		}
	}
	message[i] = '\0';
	return message;
}


char * extract_message2(int start, int stride) {
	int i, j;

	for (i = 0, j = start; 
		 *(((char *) data) + j) != '\0';
		 i++, j += stride) 
		 {
			 message[i] = *(((char *) data) + j);
		 }
	message[i] = '\0';
	return message;
}

int main (int argc, char *argv[])
{
	int dummy = 1;
	int start, stride;
	int key1, key2, key3, key4;
	char * msg1, * msg2;

	int* p = &dummy;
	int* q = &key1;
	key3 = key4 = 0;
	if (argc < 3) {
		usage_and_exit(argv[0]);
	}
	key1 = strtol(argv[1], NULL, 0);
	key2 = strtol(argv[2], NULL, 0);
	if (argc > 3) key3 = strtol(argv[3], NULL, 0);
	if (argc > 4) key4 = strtol(argv[4], NULL, 0);

	process_keys12(&key1, &key2);

	start = (int)(*((char *) &dummy));
	stride = (int)(*(((char *) &dummy) + 1));

	if (key3 != 0 && key4 != 0) {
		process_keys34(&key3, &key4);
	}

	msg1 = extract_message1(start, stride);

	if (*msg1 == '\0') {
		process_keys34(&key3, &key4);
		msg2 = extract_message2(start, stride);
		printf("%s\n", msg2);
	}
	else {
		printf("%s\n", msg1);
	}

	return 0;
}

首先看刚进入main函数的前几个int变量。

int dummy = 1;
int start, stride;
int key1, key2, key3, key4;

理论上来说他们的地址空间应该是连续的,那么实际上如何呢?

每个变量相差12个字节,也就是3个int,居然没有挨着。

再看看内存呢。

 

 可以看到的确是每个int相隔12字节,这个是编译器自己的优化,我们暂且不讨论。

但可以知道的是,我们能通过key1的地址,找到dummy的地址,并修改dummy的值。

dummy一旦被修改,start和stride也会被赋相应的值, 如此可得到message1.

再看后面,如何通过key3,key4,使程序跳过message1执行message2?

这里涉及到函数的运行机制。


概念

首先,明确几个概念。

ESP(Extended Stack Pointer):为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。

EBP(Extended Base Pointer):扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。

活跃记录(Activation Records and Stacks):栈顶的那个记录被称作活跃记录,也就是说在函数栈中,只有栈顶的记录可被程序指令访问和操作。

函数帧:执行函数所需要的信息集合,具体内容后面会提到。

程序当前地址:程序运行时当前指令所在地址,也是PC(Program Counter)程序地址寄存器中的值。

函数返回地址:函数执行完毕后应返回上一层函数继续执行指令的地址,也是指令地址。

函数调用过程

如上图所示 ,main函数中要调用两个函数,.1和2。左图中此时mian函数已经调用了2,当前的栈顶活跃记录为函数2,注意观察左图中有三个箭头,他们代表着在函数调用过程中函数栈上的三个关键指针,FramePointer对应ebp,StackPointer对应esp,或者说这两个指针的值分别存储在这两个寄存器中,另外一个没有名字的箭头,为上一层函数的FramePointer,当函数返回时,通过它来恢复调用该函数前的函数栈的信息。

再看右图,此时函数2调用函数1,此时发生的事件是:

1,保存当前FramePointer,一般保存在一个专用栈中。

2,将FramePointer指向StackPointer指向的地址。

3,将StackPointer指向栈顶。

函数返回过程

 返回过程发生的事件为:

1,将StackPointer指向FramePointer指向的地址。

2,将专用栈中保存的上一层FramePointer出栈。

以上是有关函数调用和返回的简易理解,下面是详细的细节。

函数调用过程的变量

 

 上图中有两个函数,bar为调用方,baz为被调用方。

在图一中,可以看到在真正进入baz之前,我们的指令还做了两件事:

1,将函数的参数压入函数栈。

2,将函数的返回地址压入栈中。

关于返回地址:可以看到函数的返回地址指向的是pop b指令,这条指令在call baz之后,含义是,返回地址所存储的地址值,是调用该函数的指令的下一条指令的地址,这很好理解,毕竟执行完函数还是需要回到被调用方继续执行接下来的指令。

关于函数参数:这里只有一个参数,当函数参数大于等于二得时候,参数列表的顺序和他们入栈的顺序是相反的。

更详细的信息是,函数内部所需要的局部变量存在哪里?

 

关于局部变量:由图可知,局部变量是按照顺序(不同的编译器可能不一样,但一定是在EBP和ESP之间)存在EBP和ESP之间的,并且空白的部分用0xCCCCCCCC填充,这也是为什么我们查看内存的时候经常会看到一连串的C的原因。

 到这里所有函数调用相关的知识已经介绍完了,回过头来看问题。


我们可以通过key3,key4来修改process_key34的返回地址来达到执行message2的目的。

通过上面学习到的知识我们知道函数返回地址和参数是挨着的,并且参数列表入栈的顺序和列表序是相反的,又知道是参数先入栈,返回地址后入栈,所以返回地址刚好在key3的下面,如下图所示。

这里写图片描述

 至于为什么是栈是从高地址向低地址进行的,这个是规定,记住就好。

那么偏移量显而易见了。

但是我们要将返回地址改成什么呢? 改成message2之前的process_key34地址就对了。

这里我们只能查看反汇编,查看两个process_key34结束地址的偏移量来确定二者的偏移。

到这里这道题就可以解开啦。


总结

  • 指令地址和数据地址是不一样的,不要把PC和esp,ebp这种混为一谈;返回地址返回的是PC,而esp和epb控制的是栈指针,需要区分开。
  • 总体上来说,程序运行的整个过程,是由指令来控制的,但是程序需要调用各种函数,这时就需要使用栈来管理,而esp,ebp,返回地址都是为管理这个栈和保证程序顺利运行来服务的,并且在esp和ebp之间会存有局部变量,在活跃记录的生命周期中存活。
  • 不同的编译器数据的间隔长度可能不同,要根据实际测试的情况解题。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值