首页 > 其他分享 >C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了

时间:2023-08-02 18:31:47浏览次数:41  
标签:arr 字节 int C语言 char 数组 对齐 struct

数组

数组是C语言中非常重要的一个概念,学习C语言主要就是两个知识点:数组、指针,学好这两个,那么你的C语言一定也会很好。

什么是数组?或者说什么情况下我们需要使用数组,比如说我们需要定义一个人的年龄,我们可以定义一个变量来表示,但是如果我们需要定义三个人的年龄呢?那就需要三个变量来表示,这样很复杂,那么我们是否可以使用一个变量来存储三个人的年龄呢?这时候我们就需要使用数组来定义。

数组的定义格式如下:

数据类型 变量名[常量];

在方括号中我们只能选择使用常量,而不可以选择变量,这是因为在声明的时候编译器需要提前知道数组的长度,然后才会去分配对应大小的内存;那么也就说明此处的常量是用来表示数组可存储的个数。

这里我们以之前的例子,定义一个数组来表示:张三、李四、王五的年龄:

int age[3] = {20,18,39};

除该方式外,我们还可以使用如下这种方式定义:

int age[] = {20,18,39};

我们可以简单看下反汇编,观察数组在汇编中是如何体现的:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_数据

通过反汇编,我们可以看到数组就是整体连续存储进入堆栈中,从左到右依次进入。

那么数组在内存中是如何分配的呢?在之前我们学习过很多数据类型,在这里我们以char类型举例:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_逆向分析_02

可以看见char类型在分配内存空间时,都是以4字节空间分配的,这是因为在32位操作系统中,char类型分配的空间与int类型是一样的。

这个概念实际上就是本机宽度,本机是32位操作系统也就是4字节,在32位操作系统中处理4字节数据时速度最快,这也就出现了需要字节对齐(4字节)的情况。

这里我们再来看下char、short、int类型的数组的空间具体是如何分配的:

char a[10]; // char占用一个字节,需要10个字节,但是因为4字节对齐,所以分配的就是12个字节


short b[10]; // short占用两个字节,需要20个字节,20个字节正好符合4字节对齐,所以分配的就是20个字节


int c[10]; // int占用4个字节,需要40个字节,40个字节正好符合4字节对齐,所以分配的就是40个字节

接下来我们学习一下如何存入、读取数组的数据(方括号[]内由0开始):

int age[3] = {1,2,3};

 
// 读取

int a = age[0];


int b = age[1];

 
// 赋值(存入)

age[1] = 6;

 
// 注意:在使用数组时,方括号[]中的内容可以使用表达式,而并非强制要求为常量。

思考在数组数据读时候,可以越界读取使用么?如果可以结果是什么?我们可以做个小实验:

int arr[10];


arr[10] = 100;

如上代码我们来运行则会出现这种错误:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_逆向分析_03

也就是说当我们使用越界读取时会去读取一个不存在的未知地址。

 

课外:缓冲区溢出

#include <stdio.h>
 

void Fun()

{

while(1)

 {

 printf("why?\n");

 }
}
 
 

int main()

{

int arr[8];


 arr[9] = (int)&Fun;


return0;

}

如上代码中Fun函数为什么会被调用?我们可以通过反汇编代码+堆栈图来理解:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_逆向分析_04

堆栈图如下:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_数组_05

经过观察我们发现,这里的数组越界访问,造成了堆栈中返回地址被篡改为Fun函数的地址,一旦执行到ret指令后,程序将会跳转到Fun函数然后往下执行,也就进入了死循环输出。

多维数组

多维数组是什么?假设我们现在需要定义一个班级有2个组,每个组有2个人,数组可以这样定义:

int arr[4] = {1,2,3,4};


int arr1[2*2] = {1,2,3,4};


int arr2[2][2] = {{1,2},{3,4}};

一共有三种方式,最后一种的表示我们就称之为多维数组,我们之前所学的数组我们可以称之为一维数组;为什么会使用到多维数组?
如上图代码所示,我们想要知道第二组的第二个人,可以这样调用:

arr[3];


arr1[3]


arr2[1][1];

可以很明显的看见,当我们使用一维数组去调用的时候要通过计算的方法去思考,但是使用多维数组(这里有两个方括号所以称之为二维数组)我们完全没有这种烦恼,可以很方便的去调用。

那么以上所示的多维数组在内存中的分布是怎么样的呢?我们可以通过反汇编来看一下:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_数组_06

如上图所示我们可以清晰的看见多维数组在内存中的分布是怎么样的,跟一维数组存储一点区别都没有。

所以也可以得出一个结论就是int arr[2*2];等价于int arr[2][2];

多维数组的读写也很容易理解,举例说明一年有12个月,每个月都有一个平均气温,存储5年的数据:

int arr[5][12] = {


 {1,2,1,4,5,6,7,8,9,1,2,3}, // 0


 {1,2,1,4,5,6,7,8,9,1,2,3}, // 1


 {1,2,1,4,5,6,7,8,9,1,2,3}, // 2


 {1,2,1,4,5,6,7,8,9,1,2,3}, // 3


 {1,2,1,4,5,6,7,8,9,1,2,3} // 4

};
读取第一年五月份的数据,修改第二年三月份的数据,可以这样来操作:

arr[0][4];


arr[1][2] = 10;

编译器是如何找到对应数据的呢?第一年五月份的数据 → arr[0*12+4];

结构体

思考一下:

当需要一个容器能够存储1个字节,你会怎么做?使用char

当需要一个容器能够存储4个字节,你会怎么做? 使用int

当需要一个容器能够存储100个2个字节的数据,你会怎么做? 使用short arr[100]

当需要一个容器能够存储5个数据,这5个数据中有1字节的,2字节的有10字节的...你会怎么做?

这时候我们就要学习一个新的概念叫做:结构体;结构体的定义如下:

struct 类型名{

// 可以定义多种类型


int a;


char b;


short c;

};

那么结构体的特点是什么呢?

  1. char/int/数组 等类型是编译器已知类型,我们称之为内置类型;但结构体编译器并不认识,当我们使用的时候需要告诉编译器一声,我们也称之为自定义类型;
  2. 如上代码所示我们仅仅是告诉编译器,我们定义的类型是什么样的,这段代码本身并不会占用内存空间;
  3. 结构体声明的位置和变量一样,都存在全局和局部的属性;
  4. 结构体在定义的时候,除了本身以外可以使用任何类型。

结构体类型变量的定义:

struct stStudent
{

int stucode;


char stuName[20];


int stuAge;


char stuSex;

};
 

stStudent student = {101,"张三",18,'M'};

结构体类型变量的读写:
struct stPoint
{

int x;


int y;

};
 

stPoint point = {10,20};


int x;


int y;

 
// read
x = point.x;
y = point.y;
 
// write

point.x = 100;


point.y = 200;

定义结构体类型的时候,直接定义变量:
struct stPoint
{

int x;


int y;

}point1,point2,point3;
 

point1.x = 1;


point1.y = 2;

 

point2.x = 3;


point2.y = 4;

 

point3.x = 5;


point3.y = 6;

如上代码所示,定义结构体时是分配内存的,因为不仅定义了新的类型,还定义了三个变量。
动手思考一下,如下代码是否可行?
struct stPoint
{

int x;


int y;

};
 

stPoint point = {10,20};


stPoint point2 = {11,20};

point = point2;

简单看下反汇编,我们发现是可以的,因为这里结构体类型都是一样的,类型一样,自然可以赋值:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_数组_07

字节对齐

之前我们学习过本机宽度的概念,在32位操作系统中,当我们定义变量时,当其数据宽度小于4字节时,在编译的时候还是会以4字节的方式去存储,这是一种用空间换时间的策略,那么除了本机宽度,还有字节对齐也属于这一策略。

我们先使用一段代码来测试一下:

char x;


int y;

 

int main()

{

 x = 1;


 y = 2;


return0;

}

如上x、y两个全局变量,char类型数据宽度是一个字节,所以假设其内存地址是0x0,那么全局变量y的内存地址是0x1,这是我们的猜想,但实际并不是这样,通过反汇编来看一下:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_字节对齐_08

可以清晰的看见,这里的地址并不是连续的,35、36、37这三个字节是被浪费掉了,这是为什么呢?这就是我们所谓的字节对齐;细心的人会发现这里的地址实际上就是数据宽度的整数倍,例如0x38是十进制4的整数倍。

字节对齐就是:一个变量占用N个字节,则该变量的起始地址必须是N的整数倍,即:起始地址 % N = 0;如果是结构体,那么结构体的起始地址则是其最宽的数据类型成员的整数倍;这种方式可以提升程序编译的效率。

例如如下代码:

struct Test {

int a;


char b;

};

则该结构体的起始地址则是4的整数倍,因为其最宽的数据类型成员是a(int类型)。

当我们想要打印一个变量、结构体等等的数据宽度该怎么办?这里我们需要使用到一个关键词sizeof,其使用方法如下:

我们可以以上面所示的结构体代码举例,来打印一下看看:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_字节对齐_09

结构是8,所以也印证了,我们说的结构体也是需要字节对齐的。

之前我们说了这种方式是空间换时间的策略,但是在一些场景下,我们可用的空间有限,这种空间浪费可能无法满足或者我们无法接受这种浪费,这种情况下我们可以使用如下的方式来改变这种对齐方式:

#pragma pack(1)

struct Test{

char a;


int b;

};
#pragma pack()

如上代码是通过#pragma pack(1)来改变结构体成员的对齐方式,但是无法影响结构体本身。

#pragma pack(n)中的n用来设定变量以n字节对齐方式,可以设定的值包含:1、2、4、8,VC6编译器默认是8;所以我们可以使用如上这种方式来取消强制对齐。

我们来看一下这个结构体最终的宽度是多少:

#pragma pack(2)

struct Test{

char a;


int b;


char c;

};
#pragma pack()

它的内存分配是这样的:

C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体的话最难的就是对齐了_数组_10

因为我们强制要求了以2字节的方式进行对齐,所以char类型虽然只占了一个字节,却需要分配2个地址,而结构体的宽度等于 最 小值(对齐参数, 最大数据宽度)的倍数。

结构体数组

结构体和int、char等本质是没有区别的,所以结构体也有数组,结构体数组的定义如下:
类型 变量名[常量表达式];
 
// 定义结构体类型
struct stStudent
{

int Age;


int Level;

};
 
// 定义结构体变量
struct stStudent st;
// 定义结构体数组

struct stStudent arr[10]; 或者 stStudent arr[10];

结构体数组初始化:
struct stStudent {

int Age;


int Level;

};
 

stStudent arr[5] = {{0,0}, {1,1}, {2,2}, {3,3}, {4,4}};

 

arr[0].Age = 100;


arr[0].Level = 100;

结构体成员的使用:
// 结构体数组名[下标].成员名
 

arr[0].Age = 10;


int age = arr[0].Age;

字符串成员的处理:
struct stStudent{

int Age;


char Name[0x20];

};

struct stStudent arr[3] = {{0,"张三"},{1,"李四"},{2,"王五"}};

 
// 读

char buffer[0x20];


strcpy(buffer,arr[0].Name);

 
// 写

strcpy(arr[0].Name,"王钢蛋");

strcpy是一个字符串处理函数,用于字符串拷贝,其参数是传入的是两个地址,就谁传给谁。
最后我们来看一下结构体数组的内存结构,如下代码:
struct stStudent{

int Age;


char Name[0x20];

};

struct stStudent arr[3] = {{0,"张三"},{1,"李四"},{2,"王五"}};

 

int x = arr[0].Age;

结构体 stStudent 的宽度为 8 + 32 = 40;我们观察到结构体数组在内存中是连续存储的。

标签:arr,字节,int,C语言,char,数组,对齐,struct
From: https://blog.51cto.com/u_11908275/6941641

相关文章

  • LeetCode 热题 100 之 189. 轮转数组
    题目给定一个整数数组nums,将数组中的元素向右轮转k个位置,其中k是非负数。示例1:输入:nums=[1,2,3,4,5,6,7],k=3输出:[5,6,7,1,2,3,4]解释:向右轮转1步:[7,1,2,3,4,5,6]向右轮转2步:[6,7,1,2,3,4,5]向右轮转3步:[5,6,7,1,2,3,4]示例2:输入:nums=......
  • C语言学习笔记
    C语言入门写代码流程写C代码1、创建工程2、创建项目.cpp-c++文件.c-源文件.h-头文件head3、写代码1、main主函数,程序的入口,有且仅有一个//包含一个叫stdio.h的文件//std-标准standardinnputout标准输入输出,所以函数中有输入、输出语句都要包含这个文件#in......
  • c语言作业之求两个数的最大公约数
    intmain()//最大公约数{ intn=0; intm=0; intr=0; printf("请输入两个数字:"); scanf("%d%d",&n,&m); while(n%m)//n取模m { r=n%m; n=m; m=r; } printf("最大公约数为:%d\n",m); return0;}......
  • 【C语言基础】分支和循环
    目录一、分支语句1.1if语句1.2switch语句二、循环语句2.1while语句2.2for语句2.3dowhile语句一、分支语句1.1if语句语法结构if(表达式1)//如果表达式1为真,执行语句1;如果为假,不执行。语句1;if(表达式1)//如果表达式1为真,执行语句1;如果为假,执行语句2。语句1;else语句2;//多......
  • 我的第八次C语言练习
    今天原本还想多学点,结果代码打到一半突然没保存到,导致只能重新打,浪费了很多时间,也就没学什么。//intmain(void)//{// floata;// a=3.1415926;// printf("%fcanbewritten%e\nalsocanbewritten%a",a,a,a);// return0;//}今天学的是浮点数,其中%f,%e,%a分别指的......
  • 数组去重的方法
    1、双重for循环+splice()思路:数组的splice()方法删除当前重复元素,第一个参数是开始的值,第二个参数是需要删除的个数。letarr=["a","the","a","b","test","good","the","a","good","a"]......
  • C语言, 字符串
    #include<stdio.h>#include<stdlib.h>#include<string.h>char*tt3="web数学算法";intstr_comparer(){//字符串是个指向字符串开头的指针char*tt1="aac";char*tt2="aa";//字符串可以直接转换成整数,前面加......
  • C语言学习笔记
    C语言程序设计求100-500的质数#include<stdio.h>intmain(){inti,j,n,f=1;for(i=100;i<=500;i++){f=1;for(j=2;j<i/2;j++){if(i%j==0){f=0;}}if(f==1){printf(&......
  • 【C语言】调试的运用,代码出现错误如何自己解决
    1.了解调试。什么是调试?_当我们发现程序中存在的问题的时候,那下⼀步就是找到问题,并修复问题。_这个找问题的过程叫称为调试,英⽂叫debug(消灭bug)的意思。_调试⼀个程序,⾸先是承认出现了问题,然后通过各种⼿段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的⽅式,找到问......
  • PHPHashtable 如何优化数组查找和排序
    PHPHashtable如何优化数组查找和排序PHP是一种高度流行的编程语言,被广泛用于web开发。它有很多的优点,例如易于学习、跨平台、简单易用的语法等等。而在PHP中,数组是一种非常常用的数据结构,它可以存储一组有序的数据,方便我们进行各种操作。PHPHashtable如何优化数组查找和排......