再谈C语言

本文详细介绍了从汇编语言到C语言的抽象过程,包括数据类型的封装、函数的定义以及内存布局的理解。通过示例代码展示了C语言中数组、结构体、指针的使用,并探讨了内存分配、栈帧的创建和销毁。同时,提到了GCC编译器的选项和工作原理,以及编译过程中涉及的汇编指令。文章最后讨论了C语言中可能出现的内存问题,如野指针和内存泄漏。
摘要由CSDN通过智能技术生成

我们将汇编语言有了个基本的了解。

现在我开始了解C语言。

我们知道操作汇编语言等同于操作CPU。

创造一门语言,就是为了更方便,二进制 -> 汇编 -> 高级语言

而这次的汇编 -> 高级语言

汇编在不同的CPU上,定义不同的数据类型,需要写不同的指令集,操作不同的寄存器。

那么我们能不能抽象。

将寄存器封装。将指令集封装。将数据类型封装

第一步我们将数据类型进行封装

  1. byte 字节
  2. char 字符类型
  3. short 短整型
  4. int 整型
  5. long 长整型
  6. boolean 布尔
  7. float 单精度浮点型
  8. 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 关键字的作用

  1. 修饰全局变量 (变为本文件内私有)
  2. 修饰局部变量 (变为局部变量的栈上存储,放到了数据段)
  3. 修饰函数 (函数在本文件中私有)

为了更好的了解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 的文档。

附上官网地址:

Top (Using as)

cfi 指令

这个文档中有我们想知道的 gcc 编译出代码是什么含义。

C语言的缺点:

  1. 野指针
  2. 内存泄露

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值