学完汇编以后,对类型有了一些新的理解。
类型系统的本质是如何解读内存中的一段数据。就像同样是0xFF,它到底是白色还是256还是-127,还是某个ASCII字符或者某个小数,取决于我们如何去解读它。
一个例子可以很好的说明这一点:
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
/* 机器码数组(包含可执行代码和数据) */
unsigned char machine_code[] = {
// 函数入口点
0x48, 0x8d, 0x35, 0x12, 0x00, 0x00, 0x00, // lea rsi, [rip + 0xd]
0xb8, 0x01, 0x00, 0x00, 0x00, // mov eax, 0x1
0xbf, 0x01, 0x00, 0x00, 0x00, // mov edi, 0x1
0xba, 0x0d, 0x00, 0x00, 0x00, // mov edx, 0xc
0x0f, 0x05, // syscall
0xc3, // ret
// 内嵌字符串数据 (与代码位于同一内存区域)
'H','e','l','l','o',' ','w','o','r','l','d','!', '\n' ,'\0'
};
/* 分配可执行内存 (关键步骤) */
void *exec_mem = mmap(
NULL, // 由内核选择地址
sizeof(machine_code), // 分配大小
PROT_READ | PROT_WRITE | PROT_EXEC, // 可读+可写+可执行权限
MAP_PRIVATE | MAP_ANONYMOUS, // 匿名映射(不关联文件)
-1, // 文件描述符(不使用)
0 // 偏移量
);
if (exec_mem == MAP_FAILED) {
perror("mmap failed");
return 1;
}
/* 复制机器码到可执行内存区域 */
memcpy(exec_mem, machine_code, sizeof(machine_code));
/* 将内存地址转换为函数指针 */
void (*print_func)() = (void (*)())exec_mem;
/* 执行机器码函数 */
printf("即将执行机器码函数...\n");
print_func(); // 调用机器码函数
printf("已从机器码函数返回main函数\n");
/* 清理资源 */
munmap(exec_mem, sizeof(machine_code));
return 0;
}
这段代码在Linux上运行,打印“Hello world!”。一个数组的内容是可执行的机器码,那么它被赋予执行权限后就能被当成函数。
因此,某种意义上,类型不重要,重要的是我们认为它是个什么类型,或者说它本身是个什么类型。“它本身是个什么类型”也只是它如果不是这个类型,语义就会出现问题,只要我们捏着鼻子认了新的语义,那重要的就是我们认为它是个啥类型了。