0x00前言
本文源于前几周与同事的讨论,在汇编完成后的嵌入式程序中(甚至有可能是所有编译类型的语言生成的程序下)基准的参考指针是PC?而不是单纯的绝对或者是相对寻址?
阅读本文,您可能需要掌握的知识:
技能 | 熟练度 |
---|---|
汇编语言 | 了解 |
C语言 | 熟悉 |
微机原理 | 熟悉 |
文件结构 | 了解 |
0x10 起因
刚开始,源于前几周与同事的讨论,在汇编完成后的嵌入式程序中(甚至有可能是所有编译类型的语言生成的程序下)基准的参考指针是PC?而不是单纯的绝对或者是相对寻址?
原本可能是一个及其简单的小问题。结果在同事中引起了不小的讨论。大家也都没啥具体的了解,但是觉得这个事情也蛮奇怪的,笔者这边查询了一些资料。发现这个现象也是蛮有意思的,富含程序最基础的思想,遂整理如下的文章。
0x20 思考问题的角度原因
首先,作为一个标准的程序员,这里建议读者建立一种思维方式,就是使用多种角度下对某种现象实现的方式进行建模。这也是笔者下面讨论的所有前提。作为一个程序员,一般思考角度都是在语言实现的角度上。而这个问题的原因其实是由于当前编译器的编译思维引起的。也就是说来话长了。
0x30 抽象语法树
现代的编译型语言,基本上都是三步走:预编译/预处理,编译,链接并且合并参数。
作为正经的程序员,最擅长的就是把大家都看得懂的简单语法写成大家都不懂的奇葩实现😄。比如下列代码:
#define DEFINE_DISP_TYPE_PAGE page //页面
#define DEFINE_DISP_TYPE_TXT txt //文本框
#define DEFINE_DISP_TYPE_PROGTXT prog_txt //滚动文本框
#define DEFINE_DISP_TYPE_NUMBER num //数字框
#define DEFINE_DISP_TYPE_BUTTON button //按键
#define DEFINE_DISP_TYPE_SLIPPER slipper //滑块
#define DEFINE_DISP_TYPE_PROGRESS progress //进度条
#define DEFINE_DISP_TYPE_TOUCH touch //触摸热区
#define DEFINE_DISP_TYPE_BT_SWITCH bt_switch //双态按钮
#define DEFINE_DISP_TYPE_QR_CODE qr_code //双态按钮
#define disp_set_int(OBJ,I) disp_##OBJ##_set_##I(char* obj_name,int level)\
{\
S_DISP_BASE_NODE* now = gui_private.now_page->disp_page;\
for (int i = 0; i < GUI_MAX_PAGE_COMPONENT; ++i)\
{\
if(now[i].type == DISP_TYPE_LAST)\
return;\
if(strcmp(now[i].obj_name, obj_name) == 0)\
{\
now[i].obj.OBJ.I = level;\
now[i].editor = 1;\
}\
}\
}
#define disp_set_string(OBJ,STRING) disp_##OBJ##_set_##STRING(char* obj_name,char* string)\
{\
S_DISP_BASE_NODE* now = gui_private.now_page->disp_page;\
for (int i = 0; i < GUI_MAX_PAGE_COMPONENT; ++i)\
{\
if(now[i].type == DISP_TYPE_LAST)\
return;\
if(strcmp(now[i].obj_name, obj_name) == 0)\
{\
strcpy(now[i].obj.txt.txt,string);\
now[i].editor = 1;\
}\
}\
}
#define disp_get_int(OBJ,I) disp_##OBJ##_get_##I(char* obj_name,int level)\
{\
S_DISP_BASE_NODE* now = gui_private.now_page->disp_page;\
for (int i = 0; i < GUI_MAX_PAGE_COMPONENT; ++i)\
{\
if(now[i].type == DISP_TYPE_LAST)\
return;\
if(strcmp(now[i].obj_name, obj_name) == 0)\
{\
now[i].obj.OBJ.I = level;\
now[i].editor = 1;\
}\
}\
}
#define disp_get_string(OBJ,STRING) disp_##OBJ##_get_##STRING(char* obj_name,char* string)\
{\
S_DISP_BASE_NODE* now = gui_private.now_page->disp_page;\
for (int i = 0; i < GUI_MAX_PAGE_COMPONENT; ++i)\
{\
if(now[i].type == DISP_TYPE_LAST)\
return;\
if(strcmp(now[i].obj_name, obj_name) == 0)\
{\
strcpy(now[i].obj.txt.txt,string);\
now[i].editor = 1;\
}\
}\
}
void disp_set_int(DEFINE_DISP_TYPE_TXT,pco);
void disp_set_int(DEFINE_DISP_TYPE_TXT,bco);
void disp_set_int(DEFINE_DISP_TYPE_TXT,pw);
void disp_set_int(DEFINE_DISP_TYPE_TXT,pic);
void disp_set_string(DEFINE_DISP_TYPE_TXT,txt);
void disp_set_int(DEFINE_DISP_TYPE_PROGTXT,pco);
void disp_set_int(DEFINE_DISP_TYPE_PROGTXT,bco);
void disp_set_int(DEFINE_DISP_TYPE_PROGTXT,en);
void disp_set_int(DEFINE_DISP_TYPE_PROGTXT,dir);
void disp_set_int(DEFINE_DISP_TYPE_PROGTXT,dis);
void disp_set_int(DEFINE_DISP_TYPE_PROGTXT,tim);
void disp_set_string(DEFINE_DISP_TYPE_PROGTXT,txt);
void disp_set_int(DEFINE_DISP_TYPE_NUMBER,pco);
void disp_set_int(DEFINE_DISP_TYPE_NUMBER,bco);
void disp_set_int(DEFINE_DISP_TYPE_NUMBER,val);
void disp_set_int(DEFINE_DISP_TYPE_NUMBER,lenth);
void disp_set_string(DEFINE_DISP_TYPE_BUTTON,txt);
void disp_set_int(DEFINE_DISP_TYPE_SLIPPER,pco);
void disp_set_int(DEFINE_DISP_TYPE_SLIPPER,bco);
void disp_set_int(DEFINE_DISP_TYPE_SLIPPER,val);
void disp_set_int(DEFINE_DISP_TYPE_PROGRESS,pco);
void disp_set_int(DEFINE_DISP_TYPE_PROGRESS,bco);
void disp_set_int(DEFINE_DISP_TYPE_PROGRESS,val);
void disp_set_int(DEFINE_DISP_TYPE_TOUCH,id);
void disp_set_string(DEFINE_DISP_TYPE_BT_SWITCH,txt);
void disp_set_int(DEFINE_DISP_TYPE_BT_SWITCH,val);
void disp_set_string(DEFINE_DISP_TYPE_QR_CODE,txt);
就是简单了利用宏定义,实现了这种类似于代码生成模板的操作(这里只是用来标识基本的复杂应用的方案,实际上最好使用inline的函数展开更好一些)
类似这种代码,基本上都是在预处理阶段就会被处理完毕。
随后,编译器就会根据这个代码生成一个“抽象语法树”,如果有对于编译器有些研究或者是对于指令解析器有部分实现的人都会有过这种经验:需要一种格式来描述解析到的各种数据。如数组、变量、函数、指令内的参数与外部的输入参数。
编译器的抽象语法树就是将人类能看懂的代码变成程序好处理的抽象结构。最后生成一个基本的程序雏形。
0x40切换视角——编译器
下面,请读者切换视角,想象一下自己是一个编译器开发人员(或者是编译器实体,请抛弃一切应用软件开发者的思维)。
拿到一段字符串,现在有一套比较完整的抽象语法树。那么,一个函数的结构可能是如下接口
typedef struct
{
//函数名称
char* codexxxxx_string;
//函数入口点
unsigned int codexxxxx_input;
//输入
void* value1_arg;
int value1_count;
……
void* valuexx_arg;
int valuexx_count;
//输出
void* value1_arg;
int value1_count;
……
void* valuexx_arg;
int valuexx_count;
}FUNCTION_CODE_XXXXXT;
这里就能大致看出来一个基本的结构,这样既可以满足当前函数的输入输出,也能满足当前函数作为模块化的选项的要求。需要注意的是,这里的函数入口点是一个未定义项。这项必须要到运行或者最后链接分配位置的时候才能确定,这样就可以最大程度上满足对于模块化程序的要求(各种各样的二进制类型elf库)。也就是说,编译好的程序就是一大堆电脑散件,虽然不是很散,但是实际上也不成样子。链接就是将其组成一个完整的电脑,让别人可以完成使用。
编译完成的程序模块
链接完成
下面,作为一个编译器,需要对当前函数内部进行转译,将人类喜欢的代码转换成机器喜欢的二进制数据。如果笔者对于汇编语言有所了解就应该知道一句经典语句:代码及数据,数据及代码,取决于使用者的角度。所以对于一个在编译期间还不知道自己函数入口指针的数据块,编译器要怎么处理呢?
找到一个基准指针,读者们很聪明,一下就能想到这个选项。那么,熟知CPU寄存器组成的读者应该知道,CPU寄存器基本分为三类:数据寄存器、特殊寄存器、程序指针寄存器。
数据寄存器,存放暂时使用的数据,用来存放一个基准指针代价极大(一般芯片没几个数据寄存器,每个都要用在刀刃上)
特殊寄存器,有的存堆栈,有的存一些特殊的参数(GPIO等都是)一般来说无法使用。
程序指针寄存器(PC,其他的寄存器或多或少都有别的名称,如ARM的Rx,x86的AX等,但是程序指针寄存器基本上缩写都是PC,也有叫PC指针的),存放的是当前程序运行的指针。
所以说,编译器选择了程序指针寄存器作为基准指针。于是可以看到,几乎所有程序都是这样实现的,这也是成本与效率下的几乎最优解了。
0x50 总结
不得不说,有时候这种知识完全不需要知道(因为99%的人完全不会注意到),但是通过自己查询资料知道为什么这样实现,也是蛮好玩的一件事😄。
标题:记:关于编译时为何基础偏移为PC
作者:GreenDream
地址:HTTPS://greendreamer.work/articles/2022/10/24/1666622611483.html
Comments | 0 条评论