C语言笔记 | C与汇编
发表于:2022-06-08 | 分类: C语言笔记

函数

  • 通过栈传递参数
  • 平衡栈(函数调用约定)
    1. cdecl
      • 参数从右至左入栈
      • 调用者清栈
    2. stdcall
      • 参数从右至左入栈
      • 自身清栈
    3. fastcall:
      • ecx和edx传前两个参数, 剩下的参数从右至左入栈
      • 自身清栈
  • 提升栈使用EBP寻址, 函数内使用提升后的”新”栈
    • 提升: 将原EBP地址压入栈, 使用EBP存储原ESP地址, 提升ESP
    • 下降: 将ESP复原, 取出EBP并复原
  • 返回值使用EAX传递

变量

  • 变量类型确定内存宽度
  • 变量名是内存地址的别名

全局变量

  • 每个全局变量有一个固定的独一无二的内存地址
  • 全局变量在编译的时候就已经确定了内存地址和宽度
  • 如果不重新编译, 全局变量的内存地址不变
  • 全局变量中的值任何程序都可以修改, 是公用的
  • 全局变量有默认值

局部变量

  • 局部变量是函数内部申请的, 如果函数没有执行, 那么局部变量没有内存空间
  • 局部变量的内存是在栈中分配的, 程序执行时才分配(我们无法预知函数何时执行, 这意味着无法确定局部变量的内存地址)
  • 因为局部变量的内存是不确定的, 所以只能在函数内部使用, 其他函数不能使用
  • 局部变量定义后必须初始化值

整数类型

  • 十转二: 除二取余(整数位继续除直到0, 小数位0与非0为二进制), 能保证精度
  • 使用补码存储
  • 如果数据溢出(超出范围), 将舍弃高位(截断)

浮点类型

  • 十转二: 乘二取整(小数位继续乘直到0, 整数位为二进制), 不能保证精度
  • 存储方式:
    1. float(32bit):
      • 31-30(1bit): 符号位
      • 30-22(8bit): 指数部分(第一位是科学计数法方向位, 小数点向左移是1, 向右移是0)
      • 22-0(23bit): 尾数部分
    2. double(64bit):
      • 63-62(1bit)
      • 62-51(11bit)
      • 51-0(52bit):
  • 编码步骤:
    1. 转为二进制小数
    2. 使用科学计数法表示
    3. 按编码规则填入
  • 栗子:
    • 8.25->1000.01->1.00001*2^3->0 10000010 00001000000000000000000
    • 16进制: 41040000

类型转换

  • 小转大: MOVSX(带符号)/MOVZX(不带符号)
  • 大转小: 使用小寄存器EAX/AX/AL

运算

分支语句

  • switch的效率比if-else高
    • switch在大于3个分支时, 将跳转地址存入内存, 使用jmp计算跳转
    • switch生成一张连续跳转表, 如果不连续则填充default地址, 将x减去分支中的最小值, 大于分支数则跳default, 否则映射到跳转表jmp [计算值*4+跳转表地址]
    • if-else是遍历条件执行

数组

数组越界问题

  • 数组下标+2(从栈顶开始)可以用下标找到[ebp+4]

多维数组

  • 内存连续的一维数组
  • 第1个下标表示第几个数组, 第2个下标表示每个数组中的第几个元素
  • 第1个下标每加1相当于内存中加上每个数组的长度
  • 二维数组与一维数组的映射: 从0-N所在的一维数组之间的数组个数乘一维数组的长度加上N

转换

二位数组坐标转一维数组坐标

指针

  1. 指针类型的变量宽度永远是4字节(32位)
  2. 指针类型的变量加减运算的实际宽度是它基础类型的宽度(寻址宽度)(去掉一个*)
    1. char* a = (char*) 1; a++; //a实际加了1
    2. int* b= (int*) 1; b++; //b实际加了4
    3. char** c (char**) 1; c++; //c实际加了4
  3. 取地址(&):
    1. 局部变量: lea eax, dword ptr ss:[ebp-4]
    2. 全局变量: mov eax, 00426d9c
  4. 取值(*):
    1. 读: mov eax, dword ptr ds:[eax]
    2. 写: mov dword ptr ds:[eax], 1
    • 取值后的类型是指针类型去掉一个*的类型, 栗如int (*)[2]类型取值的类型是int [2]

字符串

  1. char a[] = {‘A’, ‘B’, ‘\0’}
    • 直接赋值数组(没有常量区字符串, 数组可修改)
  2. char b[] = “AB”
    • 将字符串放到常量区
    • 将字符串复制到数组(有常量区字符串, 数组可修改)
  3. char* c = “AB”
    • 将字符串放到常量区
    • 将地址给c(有常量区字符串, 字符串不可修改)

指针与指针指向的类型没有关系

1
2
3
4
5
6
7
8
9
10
11
12
13
struct st {
int a;
int b;
}

int arr[8] = {1, 2, 3, 4, 5, 6, 7, 8};

struct st* sx = (struct st*) arr;

for (int i = 0; i < 4; i++) {
printf("%d %d\n", sx->a, sx->b);
sx++;
}

数组与数组指针类型

  • 数组是数组类型(type [length]), 不是数组指针类型(type (*)[length]), 只是数组类型的值是地址
    • 数组类型(数组名)增减是数组内数据类型的宽度
    • 数组指针类型(&数组名)增减是数组宽度(数组长度*数据类型宽度)

栗子: 二维数组只是表象

  • 数组指针的维度与数组的维度没有关系

指针偏移取一维数组类型

1
2
int a1[6] = {5, 6, 7, 4, 3, 2};
int (*p)[3] = (int (*)[3]) &a1;
  • p的宽度是3(对于int数组来说), p[1]等于*(p + 1), 位置+3到”4”的位置
  • *(p + 1)取值得到int数组类型, 再偏移取值p[1][1]或*(*(p + 1) + 1)

二维数组类型

1
2
int a1[6] = {5, 6, 7, 4, 3, 2};
int (*p)[2][3] = (int (*)[2][3]) &a1;
  • *p得到二维数组类型, 操作二维数组类型取值(*p)[1][1]

指针偏移取二维数组类型

1
2
int a1[8] = {5, 6, 7, 4, 3, 2, 1, 8};
int (*p)[2][2] = (int (*)[2][3]) &a1;
  • p的宽度是4(对于int数组来说), *(p + 1)使位置到”3”的位置
  • *(p + 1)取值得到int二维数组类型, 对后四个数操作, (*(p + 1))[1][1]

结构体

  • 对结构体的操作并不是转为指针操作(数组), 而是结构体类型
    1. 结构体作为函数参数将进行传值(所有成员)而不是传地址
      • 传一个四个成员的结构体与传四个参数没有区别
    2. 结构体的相互赋值将拷贝所有结构体成员
  • 问题: 大量的内存复制
    • 使用指针传递结构体
上一篇:
荣耀9X降级至9.1.1.120详细教程
下一篇:
C语言笔记 | Struct结构体