我们将汇编语言有了个基本的了解。
现在我开始了解C语言。
我们知道操作汇编语言等同于操作CPU。
创造一门语言,就是为了更方便,二进制 -> 汇编 -> 高级语言
而这次的汇编 -> 高级语言
汇编在不同的CPU上,定义不同的数据类型,需要写不同的指令集,操作不同的寄存器。
那么我们能不能抽象。
将寄存器封装。将指令集封装。将数据类型封装
第一步我们将数据类型进行封装
- byte 字节
- char 字符类型
- short 短整型
- int 整型
- long 长整型
- boolean 布尔
- float 单精度浮点型
- double 双精度浮点型
在数据存储类型中,我们可以这么分:
类型相同的数据:C语言的数组
类型不同的数据:C语言的结构体
我们来观察一下:
之前提过这张图,现在我么将这个细化一下。
我们现在有个两个函数。对比C语言。
第二步:封装指令
一段指令执行时,需要有人调用他,并给出自己的首地址,让别人知道自己在拿,调用当前指令片段后还需要返回继续执行,就存储一个返回地址,那么两个指令片段之间需要数据共享呢这里就需要参数传递,我们将一段指令执行过程进行封装,称为函数。不再考虑参数使用栈结构,还是寄存器传递。
但是两个函数之间,需要共享一片数据怎么办。在汇编中,如果一段指令片段中向另一个指令片段共享数据,则只需要传递 存放数据的首地址即可。使用 lea 指令,执行取地址操作。我们叫他为指针
而在 C语言中,我们使用 * 来表示取地址。& 则是 (%esp) 解引用,() 表示地址。
现在我们抽象好了C语言,并使用C语言编写如下的程序。
第一个程序:
#include <stdio.h> int main() { /* 我的第一个 C 程序 */ printf("Hello, World! \n"); return 0; }
因为C语言最后还是需要编程机器语言去执行。那么我们就需要将C语言转化为机器语言。
现在汇编语言已经有汇编器将汇编编译成机器语言,而我们只需要将C语言编译成汇编语言即可。
所以我们现在需要引入一个编译器,将C语言编译成汇编,现在开源的GCC就是干这事的。
我就需要学习一下GCC
使用 Shell 命令:
gcc --help 用法:gcc [选项] 文件... 选项: -pass-exit-codes 在某一阶段退出时返回最高的错误码 --help 显示此帮助说明 --target-help 显示目标机器特定的命令行选项 --help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...] 显示特定类型的命令行选项 (使用‘-v --help’显示子进程的命令行参数) --version 显示编译器版本信息 -dumpspecs 显示所有内建 spec 字符串 -dumpversion 显示编译器的版本号 -dumpmachine 显示编译器的目标处理器 -print-search-dirs 显示编译器的搜索路径 -print-libgcc-file-name 显示编译器伴随库的名称 -print-file-name=<库> 显示 <库> 的完整路径 -print-prog-name=<程序> 显示编译器组件 <程序> 的完整路径 -print-multiarch Display the target's normalized GNU triplet, used as a component in the library path -print-multi-directory 显示不同版本 libgcc 的根目录 -print-multi-lib 显示命令行选项和多个版本库搜索路径间的映射 -print-multi-os-directory 显示操作系统库的相对路径 -print-sysroot 显示目标库目录 -print-sysroot-headers-suffix 显示用于寻找头文件的 sysroot 后缀 -Wa,<选项> 将逗号分隔的 <选项> 传递给汇编器 -Wp,<选项> 将逗号分隔的 <选项> 传递给预处理器 -Wl,<选项> 将逗号分隔的 <选项> 传递给链接器 -Xassembler <参数> 将 <参数> 传递给汇编器 -Xpreprocessor <参数> 将 <参数> 传递给预处理器 -Xlinker <参数> 将 <参数> 传递给链接器 -save-temps 不删除中间文件 -save-temps=<arg> 不删除中间文件 -no-canonical-prefixes 生成其他 gcc 组件的相对路径时不生成规范化的 前缀 -pipe 使用管道代替临时文件 -time 为每个子进程计时 -specs=<文件> 用 <文件> 的内容覆盖内建的 specs 文件 -std=<标准> 指定输入源文件遵循的标准 --sysroot=<目录> 将 <目录> 作为头文件和库文件的根目录 -B <目录> 将 <目录> 添加到编译器的搜索路径中 -v 显示编译器调用的程序 -### 与 -v 类似,但选项被引号括住,并且不执行命令 -E 仅作预处理,不进行编译、汇编和链接 -S 编译到汇编语言,不进行汇编和链接 -c 编译、汇编到目标代码,不进行链接 -o <文件> 输出到 <文件> -pie Create a position independent executable -shared Create a shared library -x <语言> 指定其后输入文件的语言 允许的语言包括:c c++ assembler none ‘none’意味着恢复默认行为,即根据文件的扩展名猜测 源文件的语言 以 -g、-f、-m、-O、-W 或 --param 开头的选项将由 gcc 自动传递给其调用的 不同子进程。若要向这些进程传递其他选项,必须使用 -W<字母> 选项。 报告程序缺陷的步骤请参见: <http://bugzilla.redhat.com/bugzilla>.
我们选择使用 gcc -S 编译到汇编语言,不进行汇编和链接
.file "demo.c" .section .rodata .LC0: .string "Hello, World! " .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %edi call puts movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)" .section .note.GNU-stack,"",@progbits
代码中出现 .cfi_def_cfa_offset ,我们不需要这个。所以我们使用 -fno-asynchronous-unwind-tables 将其去掉
gcc -S -fno-asynchronous-unwind-tables
我们来认识一下这段汇编表示什么含义
.file "demo.c" // 文件名称 .section .rodata .LC0: .string "Hello, World! " .text // 代码段 只读 数据为何数据代码段? .globl main // globl 全局 .type main, @function // 类型 函数 main: // 函数首地址 pushq %rbp // 开辟栈帧 movq %rsp, %rbp // 开辟栈帧 movl $.LC0, %edi // 将字符串移动到 edi 寄存器中 call puts // 调用 puts 函数 movl $0, %eax // 将 0 移动到 eax 寄存器中,清空 eax 寄存器用来存放函数返回值 popq %rbp // 销毁栈 ret // 销毁栈 .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)" .section .note.GNU-stack,"",@progbits
咱们把汇编代码copy 出来
64位机: .file "test.c" .section .rodata // 只读 .LC0: .string "%d" .text // 代码段 .globl func // 全局 .type func, @function // 类型,函数 func: pushq %rbp // 开辟栈帧 movq %rsp, %rbp // 开辟栈帧 movl $10, %esi // 将10移动到 edi movl $.LC0, %edi // 将 %d 移动到 edi movl $0, %eax // 将 0 移动到 eax 清空 call printf // 调用 printf popq %rbp // 销毁栈帧 ret // 销毁栈帧 .size func, .-func .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp // 开辟栈帧 movl $0, %eax // 将 0 移动到 eax ,清空 call func // 调用 func movl $1, %eax // 移动 1 到 eax 寄存器 将返回值放到 eax 中 popq %rbp // 销毁栈帧 ret // 销毁栈帧 32位机 : .file "test.c" .section .rodata .LC0: .string "%d" .text .globl func .type func, @function func: pushl %ebp movl %esp, %ebp subl $24, %esp movl $10, 4(%esp) movl $.LC0, (%esp) call printf leave ret .size func, .-func .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp call func movl $1, %eax leave ret
根据图和代码的对比,我们来梳理一下流程。
在main函数使用 (%ebp)%rbp 和 (%esp) %rsp 开辟栈帧,一个表示栈顶 sp,一个表示栈底 bp。请移步Intel 开发手册查询
我们来看一下全局定义一个全局变量
int data = 0; // 0 表示空,未初始化的数据 int main() { printf("%s", "hello,wlord"); return 1; }
.file "globl.c" .globl data // 全局 .bss .align 4 .type data, @object .size data, 4 data: // 首地址 .zero 4 .section .rodata .LC0: // 字符串首地址 .string "hello,wlord" .LC1: .string "%s" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp // 32位模式下,-16与操作清空低地址 subl $16, %esp // 开辟空间 -16 movl $.LC0, 4(%esp) // 取地址 + 4 movl $.LC1, (%esp) call printf movl $1, %eax leave ret
#include<stdio.h> int data = 1; int main() { printf("%s", "hello,wlord"); return 1; }
.file "globl.c" .globl data .data // 只读 ,读写数据段 .align 4 .type data, @object .size data, 4 data: .long 1 .section .rodata .LC0: .string "hello,wlord" .LC1: .string "%s" .text // 代码段 .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl $.LC0, %esi movl $.LC1, %edi movl $0, %eax call printf movl $1, %eax popq %rbp ret
在当前文件中只声明不赋值
int data; int main() { printf("%d", data); return 1; }
.file "globl.c" .comm data,4,4 .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl data(%rip), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax popq %rbp ret
使用关键字 extern,声明一个外部引用。
extern int data; int main() { printf("%d", data); return 1; }
.file "globl.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp movl data, %eax movl %eax, 4(%esp) movl $.LC0, (%esp) call printf movl $1, %eax leave ret
如果运行,会抛出 data 找不到引用
/tmp/cc29oj6A.o: In function `main':
globl.c:(.text+0x6): undefined reference to `data'
collect2: error: ld returned 1 exit status
那我们再定义的一个文件,data.c,进行一起编译 gcc -s globl.c data.c
int data = 33;
.file "globl.c" .comm data,4,4 .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl data(%rip), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax popq %rbp ret
当我们在 data.c 文件中添加 static 关键字。
则有弹出这个异常,说明 static 关键字的作用就是起到文件内私有。只有当前为文件可以访问。
/tmp/ccU5Mf1m.o: In function `main':
globl.c:(.text+0x6): undefined reference to `data'
collect2: error: ld returned 1 exit status
现在我们继续观察 数组和结构体
int main() { int arr[] = {1, 3, 4}; printf("%d",arr); return 1; }
.file "dataArr.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) // 开辟空间 movl $3, -12(%rbp) movl $4, -8(%rbp) leaq -16(%rbp), %rax movq %rax, %rsi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
我们看到 lea 指令,取地址也就是说 arr 保存了一个地址。那么我们修改一下代码
int main() { int arr[] = {1, 3, 4}; printf("%d",arr[0]); return 1; }
.file "dataArr.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) movl $3, -12(%rbp) movl $4, -8(%rbp) movl -16(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
由此我们可见 arr 和 arr[0] 编译出来是一致的。那我们使用地址进行运算他会出现什么情况呢。
int main() { int arr[] = {1, 3, 4}; printf("%s",arr + 1); return 1; }
.file "dataArr.c" .section .rodata .LC0: .string "%s" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) movl $3, -12(%rbp) movl $4, -8(%rbp) leaq -16(%rbp), %rax addq $4, %rax movq %rax, %rsi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
int main() { int arr[] = {1, 3, 4}; printf("%s",arr[0] + 1); return 1; }
.file "dataArr.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) movl $3, -12(%rbp) movl $4, -8(%rbp) movl -16(%rbp), %eax addl $1, %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
int main() { int arr[] = {1, 3, 4}; printf("%d \n", *(arr + 1)); // +1 表示当前地址偏移一个 int 单位的 printf("%d", arr[1]); return 1; }
当前代码输出都是 3 , 也就是说 *(arr) = arr[0], *(arr + 1) = arr[1] ,*(arr + 2) = arr[2]
我们来观察一个结构体。
struct Sdu { int a; int b; char arr[]; }; int main() { struct Sdu sdu = {1, 2}; printf("%d", sdu); return 1; }
.file "dataStruct.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) movl $2, -12(%rbp) movq -16(%rbp), %rsi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
我们观察到 char[] 并没有开辟内存。sdu 也是表示地址
struct Sdu { int a; int b; char arr[]; }; int main() { struct Sdu sdu = {1, 2}; printf("%d", sdu + 1); return 1; }
dataStruct.c:9:21: error: invalid operands to binary + (have ‘struct Sdu’ and ‘int’)
printf("%d", sdu + 1);
struct Sdu { int a; int b; char arr[]; }; int main() { struct Sdu sdu = {1, 2}; printf("%d", *(&sdu.a+ 1)); // &取地址 sdu.a 然后 偏移 一个单位 * 再将当前地址中的值取出来。 printf("%d", sdu.b); return 1; }
当前输出结果都是2
.file "dataStruct.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) movl $2, -12(%rbp) leaq -16(%rbp), %rax addq $4, %rax // *(&sdu.a+ 1) -16+4 =-12 movl (%rax), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl -12(%rbp), %eax // sdu.b movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
我们将字符串进行初始化。
struct Sdu { int a; int b; char arr[4]; }; int main() { struct Sdu sdu = {1, 2, "1234"}; printf("%d", *(&sdu.a+ 1)); printf("%d", sdu.b); return 1; }
.file "dataStruct.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -16(%rbp) movl $2, -12(%rbp) movl $875770417, -8(%rbp) // 这里对比刚才 开辟了字符的空间 875770417 表示 "1234" leaq -16(%rbp), %rax addq $4, %rax movl (%rax), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl -12(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave ret
struct Sdu { int a; int b; char arr[10]; }; int main() { struct Sdu sdu = {1, 2, "1234"}; printf("%d", *(&sdu.a+ 1)); printf("%d", sdu.b); return 1; }
.file "dataStruct.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $32, %rsp movl $1, -32(%rbp) movl $2, -28(%rbp) movq $875770417, -24(%rbp) // "1234" movw $0, -16(%rbp) // 数组长度为10 使用了4应该还剩6,但是开辟了8,可能是内存对齐 leaq -32(%rbp), %rax addq $4, %rax movl (%rax), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl -28(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave
那么我就试一下,使用数组长为12时,他会开辟多少空间。
struct Sdu { int a; int b; char arr[12]; }; int main() { struct Sdu sdu = {1, 2, "1234"}; printf("%d", *(&sdu.a+ 1)); printf("%d", sdu.b); return 1; }
.file "dataStruct.c" .section .rodata .LC0: .string "%d" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp subq $32, %rsp movl $1, -32(%rbp) movl $2, -28(%rbp) movq $875770417, -24(%rbp) movl $0, -16(%rbp) // 还是开辟 8 的空间。 leaq -32(%rbp), %rax addq $4, %rax movl (%rax), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl -28(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax leave
稍后我们来讲解一个内存对齐的问题。
#include<stdio.h> struct Stu { int age; char name[4]; }; int main() { char charData = 1; int intData = 1; int arr[2] = {1, 2}; char arrStr[2] = {"12"}; struct Stu stu = {1, "adcc"}; printf("%d\n", sizeof(charData)); printf("%d\n", sizeof(intData)); printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(arrStr)); printf("%d\n", sizeof(stu)); return 1; } 输出: 1 4 8 2 8 #include<stdio.h> struct Stu { int age; char name[4]; }; int main() { char charData = 1; int intData = 1; int arr[2] = {1, 2}; char arrStr[2] = {"12"}; struct Stu stu = {1, "adcc"}; printf("%d\n", sizeof(charData)); printf("%d\n", sizeof(intData)); printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(arrStr)); printf("%d\n", sizeof(stu)); return 1; } 输出: 1 4 12 3 12 #include<stdio.h> struct Stu { int age; char name[8]; }; int main() { char charData = 1; int intData = 1; int arr[3] = {1, 2}; char arrStr[3] = {"12"}; struct Stu stu = {1, "adcc"}; printf("%d\n", sizeof(charData)); printf("%d\n", sizeof(intData)); printf("%d\n", sizeof(arr[2])); printf("%d\n", sizeof(arrStr[2])); printf("%d\n", sizeof(stu.name)); return 1; } 输出: 1 4 4 1 8
观察可得,sizeof 如果传入的参数是指针的话,他们它将计算的当前指针所指内存区域所开辟的空间大小
如果传入的基础类型,这获得的是基础数据类型的开辟的空间大小
struct Stu { int age; char name[8]; }; typedef char* sds; int main() { char charData = 1; int intData = 1; int arr[3] = {1, 2}; char arrStr[3] = {"12"}; struct Stu stu = {1, "adcc"}; printf("%d\n", sizeof(charData)); printf("%d\n", sizeof(intData)); printf("%d\n", sizeof(arr[2])); printf("%d\n", sizeof(arrStr[2])); printf("%d\n", sizeof(stu.name)); printf("%d\n", sizeof(struct Stu)); sds c = arrStr; sds i = arr; printf("%d\n", sizeof(c)); printf("%d\n", sizeof(i)); return 1; } 输出: 1 4 4 1 8 12 // sizeof(struct Stu) 8 8
typedef 用于为复杂的变量名起别名。
指针 - 指向的是地址,但是这个地址中存的是什么类型数据,只要取出时转化正确即可。如上例子,可以使用不同类型的指针赋值不同类型的值。
现在我们开始了认识了指针在汇编代码是什么含义,我们顺便来观察一下Linux 源码和 Redis 源码中的使用
如下是 Redis 2.6的源码:
typedef char *sds; struct sdshdr { int len; int free; char buf[]; }; static inline size_t sdslen(const sds s) { // 当前 S 是一个地址 *sds // sizeof(结构体) 获取到结构的整体长度 // 使用地址减去 结构体的长度 = s.len 的起始位置 // (void*) 转换为指针类型,接受时转化为正确的类型 struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->len; } static inline size_t sdsavail(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->free; }
这里有个知识点会用到,内存的数据分布是由CPU决定的,也就是大端存储和小端存储,而在微型处理器 CPU 基本都是小端存储,而小端存储的特点就是
数据的高字节存储在高地址中,数据的低字节存储在低地址中 例如:
0x12345678
按低字节到高字节的存储顺序为0x78、0x56、0x34和0x12
所以在内存中感觉就像是倒着放
如下是 Linux 0.11 的源码
#include<stdio.h> #include<stdlib.h> #include<string.h> int main() { char charData = 1; printf("%d\n", sizeof(charData)); typeof(charData) a = charData; printf("%d\n", a); return 1; } 输出: 1 1 typeof(charData) a
/** * container_of - cast a member of a structure out to the containing structure * * @ptr: the pointer to the member. member的指针 * @type: the type of the container struct this is embedded in. 类型 * @member: the name of the member within the struct. 在结构体中的名字 * */ #define container_of(ptr, type, member) ( { const typeof( ((sdshdr *)0) -> buf ) *__mptr = (buf); // typeof 获取当前变量的类型,或者函数返回值的类型 // typeof(((sdshdr *)0) -> buf) 那么这个的处理之后就是 char 数组 (type *) // struct sdshdr ( // 相当于(void*) (s-(sizeof(struct sdshdr))) // 从而获取到结构体的首地址 (char *) __mptr - offsetof(sdshdr, buf) ); } ) // (0 - sdshdr.buf) 的 size #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
使用Redis 解释 这段代码
#define container_of(ptr, type, member) ( { const typeof( ((type *)0) -> member ) *__mptr = (ptr); (type *) ( (char *) __mptr - offsetof(type,member) ); } ) #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
我们再来看看字符串
#include<stdio.h> #include<stdlib.h> #include<string.h> int main() { printf("%d\n", sizeof("hello")); printf("%d\n", strlen("hello")); return 1; } 输出: 6 // sizeof() 是 6 5 // strlen() 是 5
这是我们就有疑问了,为何长度是 6 字符长度确实 5,它有一个字符占了一位,猜一猜这个字符是干什么用的。
这里提出一个疑问,如何判断一个是字符串的结尾,使用什么做结束标识,这里其实就是这个功能,而这个字符就 \0 那我们怎么才能 验证这个猜想是正确的呢,那我们就主动给字符串后面增加一个 \0 看他输出什么。
#include<stdio.h> #include<stdlib.h> #include<string.h> int main() { printf("%d\n", sizeof("hello\0 23123213")); printf("%d\n", strlen("hello\0 21321312312")); return 1; } 输出: 16 5 //字符串长度还是5
.file "str.c" .section .rodata .LC0: .string "%d\n" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl $16, %esi movl $.LC0, %edi movl $0, %eax call printf movl $5, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax popq %rbp ret
并且 sizeof strlen 在编译期就计算完成了。
即刚才的字符串,我们看下Redis在操作字符时干了什么?
sds sdsnewlen(const void *init, size_t initlen) { struct sdshdr *sh; // 堆空间分配内存 if (init) { // 为何在这里 +1 sdshdr 开辟结构体的空间大小 + 字符串的长度 + 一个分隔符 sh = zmalloc(sizeof(struct sdshdr)+initlen+1); } else { sh = zcalloc(sizeof(struct sdshdr)+initlen+1); } if (sh == NULL) return NULL; sh->len = initlen; sh->free = 0; // 内存空间字符串的赋值 if (initlen && init) memcpy(sh->buf, init, initlen); // 这里使用 \0 做结尾。说明了什么? sh->buf[initlen] = '\0'; return (char*)sh->buf; } sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); }
#include<stdio.h> int fun() { static int a = 1; return a++; } int main() { printf("%d\n", fun()); printf("%d\n", fun()); return 1; } 输出: 1 2
.file "test.c" .text .globl fun .type fun, @function fun: pushq %rbp movq %rsp, %rbp movl a.2178(%rip), %eax leal 1(%rax), %edx movl %edx, a.2178(%rip) popq %rbp ret .LFE0: .size fun, .-fun .section .rodata .LC0: .string "%d\n" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl $0, %eax call fun movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax call fun movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $1, %eax popq %rbp ret .LFE1: .size main, .-main .data .align 4 .type a.2178, @object .size a.2178, 4 a.2178: .long 1 .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)" .section .note.GNU-stack,"",@progbits
这个是 C语言的闭包,如果 static 在 函数的方法内使用,当前变量不会随着栈的销毁和消失,他会存放到 .data 段中
总结 static 关键字的作用
- 修饰全局变量 (变为本文件内私有)
- 修饰局部变量 (变为局部变量的栈上存储,放到了数据段)
- 修饰函数 (函数在本文件中私有)
为了更好的了解C语言,我们就需要了解编译器,编译约束着C语言的特性,语言的语法编译器识别才能得到想要的结构,而编译器编译出的代码是汇编代码,所以我们得先了解汇编代码,所以我们得去了解汇编语言的规范,而汇编器就是这些规范的实现。而现在有两个平台,一个是 Intel 一个是 AT&T (GAS)
这里我们选择 GAS 。
现在我们去浏览一下 GAS 的文档。
gcc -v demo.c Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper Target: x86_64-redhat-linux Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux Thread model: posix gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' /usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1 -quiet -v str.c -quiet -dumpbase str.c -mtune=generic -march=x86-64 -auxbase str -version -o /tmp/ccQG0On3.s // 编译版本号 GNU C (GCC) version 4.8.5 20150623 (Red Hat 4.8.5-44) (x86_64-redhat-linux) compiled by GNU C version 4.8.5 20150623 (Red Hat 4.8.5-44), GMP version 6.0.0, MPFR version 3.1.1, MPC version 1.0.1 GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include-fixed" ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/include" #include "..." search starts here: #include <...> search starts here: /usr/lib/gcc/x86_64-redhat-linux/4.8.5/include /usr/local/include /usr/include End of search list. GNU C (GCC) version 4.8.5 20150623 (Red Hat 4.8.5-44) (x86_64-redhat-linux) compiled by GNU C version 4.8.5 20150623 (Red Hat 4.8.5-44), GMP version 6.0.0, MPFR version 3.1.1, MPC version 1.0.1 GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 Compiler executable checksum: 231b3394950636dbfe0428e88716bc73 COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' // 命令 生成临时文件 as -v --64 -o /tmp/cc36Caxy.o /tmp/ccQG0On3.s GNU assembler version 2.27 (x86_64-redhat-linux) using BFD version version 2.27-34.base.el7 COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/ LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/ COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' /usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. /tmp/cc36Caxy.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o // 为了看这个文件是啥,我们可以执行如下命令 as -v --64 -o str.o str.s // 最后它将 str.o 文件输出
现在我们下载 GS 的文档。
附上官网地址:
cfi 指令
这个文档中有我们想知道的 gcc 编译出代码是什么含义。
C语言的缺点:
- 野指针
- 内存泄露