C语言与汇编

部分过程可参考C++ primer plus

【C语言与汇编】简单学学C到汇编代码_数组

本书余下的篇幅讨论源代码文件中的内容;本节讨论创建源代码文 件的技巧。有些C++实现(如Microsoft Visual C++、Embarcadero C++ Builder、Apple Xcode、Open Watcom C++、Digital Mars C++和Freescale CodeWarrior)提供了集成开发环境(integrated development environments,IDE),让您能够在主程序中管理程序开发的所有步骤, 包括编辑。有些实现(如用于UNIX和Linux的GNU C++、用于AIX的 IBM XL C/C++、Embarcadero分发的Borland 5.5免费版本以及Digital Mars编译器)只能处理编译和链接阶段,要求在系统命令行输入命令。 在这种情况下,可以使用任何文本编辑器来创建和修改源代码。例如, 在UNIX系统上,可以使用vi、ed、ex或emacs;在以命令提示符模式运 行的Windows系统上,可以使用edlin、edit或任何程序编辑器。如果将 文件保存为标准ASCII文本文件(而不是特殊的字处理器格式),甚至 可以使用字处理器。另外,还可能有IDE选项,让您能够使用这些命令 行编译器。

编译和链接

Stroustrup实现C++时,使用了一个C++到C的编译器程序, 而不是开发直接的C++到目标代码的编译器。前者叫做cfront(表示C前 端,C front end),它将C++源代码翻译成C源代码,然后使用一个标准 C编译器对其进行编译。这种方法简化了向C的领域引入C++的过程。其 他实现也采用这种方法将C++引入到其他平台。随着C++的日渐普及, 越来越多的实现转向创建C++编译器,直接将C++源代码生成目标代 码。这种直接方法加速了编译过程,并强调C++是一种独立(虽然有些 相似)的语言。

Linux系统中最常用的编译器是g++,这是来自Free Software Foundation的GNU C++编译器。Linux的多数版本都包括该编译器,但并 不一定总会安装它。g++编译器的工作方式很像标准UNIX编译器。例 如,下面的命令将生成可执行文件a.out

目前有些不太理解的是int类型的长度居然是可变的。

C++的基本类型分为两组:一组由存储为整数的值组成,另一组由 存储为浮点格式的值组成。整型之间通过存储值时使用的内存量及有无 符号来区分。整型从最小到最大依次是:bool、char、signed char、 unsigned char、short、unsigned short、int、unsigned int、long、unsigned long以及C++11新增的long long和unsigned long long。

还有一种wchar_t 类型,它在这个序列中的位置取决于实现。C++11新增了类型char16_t 和char32_t,它们的宽度足以分别存储16和32位的字符编码。C++确保 了char足够大,能够存储系统基本字符集中的任何成员,而wchar_t则可 以存储系统扩展字符集中的任意成员,short至少为16位,而int至少与 short一样长,long至少为32位,且至少和int一样长。确切的长度取决于实现
字符通过其数值编码来表示。I/O系统决定了编码是被解释为字符 还是数字。

浮点类型可以表示小数值以及比整型能够表示的值大得多的值。3 种浮点类型分别是float、double和long double。C++确保float不比double 长,而double不比long double长。通常,float使用32位内存,double使用 64位,long double使用80到128位。

通过提供各种长度不同、有符号或无符号的类型,C++使程序员能 够根据特定的数据要求选择合适的类型。

quick start

目录:/work

gcc -m32 -S hello.c  # 只编译生成汇编代码片段,且通过 32 位的模式生成
gcc -S hello.c
gcc -S -fno-asynchronous-unwind-tables # 去除生成的 针对debug 使用的信息

hello程序

#include <stdio.h>

int main()
{
printf("Hello, World! \n");
return 0;
}
.file "hello.c"            ;表明当前代码文件
.section .rodata ;一个小节,rodata 只读数据段.除开数据段还有只读数据段
.LC0:
.string "Hello, World! "
.text
.globl main
.type main, @function
main:
.LFB0:
pushq %rbp
movq %rsp, %rbp
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
ret
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits

解释:

从上面的代码中我们可以看见,在进入 ​​mian 函数​​​后首先会处理 ​​rbp ​​​和​​ rsp​​​,并且在调用 ret 之前会先将 ​​rbp ​​​的值恢复。(注意:上面的代码是 64位机上编译的代码,所以 寄存器是 r 开头,表示 64位)
lea指令是啥意思

除此之外,我们还验证了一个东西:返回值是通过 ax 寄存器存储的

通过命令生成汇编代码如下(64bit):

.file   "hello.c" 
.text
.section .rodata
.LC0: ;任何英文然后+: 就是一个地址 字符串首地址
.string "Hello, World! " ;string类型, 值为 hello,world
.text ;代码段
.globl main ;全局符号名字 main 全局范围
.type main, @function ;类型为 方法、函数
main: ;任何英文然后+: 就是一个地址 main函数首地址
endbr64
pushq %rbp ;将此时的rbp压栈
movq %rsp, %rbp ; rbp = rsp
movl $.LC0, %edi
call puts
movl $1, %eax ;将函数的返回值 放入到 eax 中【约定俗成】
popq %rbp ;rbp = pop
ret
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits

32位的

.file   "hello.c"                      ;表明当前代码文件
.text
.section .rodata ;一个小节,rodata 只读数据段
.LC0: ;任何英文然后+: 就是一个地址
.string "Hello, World! " ;string类型, 值为 hello,world
.text ;代码段
.globl main ;全局符号名字 main 全局范围
.type main, @function ;类型为 方法、函数
main:
pushl %ebp ;将此时的rbp压栈
movl %esp, %ebp ; rbp = rsp
// 开辟main函数栈帧

andl $-16, %esp ;与操作符号,将低位都变为0,清空的过程
subl $16, %esp ;开辟空间

movl $.LC0, (%esp) ;将字符串的地址保存在esp指向的内存单元中
call puts
movl $1, %eax ;将返回值 放入到 eax 中
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits

验证值传递代码

关键词是函数调用,可以参考一下视频进行学习。

​https://www.bilibili.com/video/BV1RS4y1B75v​

​https://www.bilibili.com/video/BV1Nt4y1G728​

值传递

#include <stdio.h>

int main()
{
int i = 1;
fun(i);
return 0;
}

void fun(int a)
{
a = a + 1;
}
.file "paramTrans.c"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp ;int i = 1 开辟栈空间并存放值
movl $1, -4(%rbp)
movl -4(%rbp), %eax ;存放参数,通过 eax 中转
movl %eax, %edi
movl $0, %eax
call fun
movl $0, %eax
leave
ret
.size main, .-main
.globl fun
.type fun, @function
fun:
pushq %rbp ;开辟栈帧
movq %rsp, %rbp
movl %edi, -4(%rbp) ;获取参数
addl $1, -4(%rbp) ;执行代码
popq %rbp ;恢复RBP
ret
.size fun, .-fun
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits

通过上述代码和注释,可以看出,此时是使用的是 ​​寄存器%edi​​ 进行传参

栈传值

#include <stdio.h>

int main()
{
int i = 1;
fun(i,i,i,i,i,i,i,i,i,i,i,i,i);
return 0;
}

void fun ( int a, int b, int c, int d, int e, int f, int h, int i, int j, int k, int l, int m, int n)
{
a = a + b + c + d + e + f + h + i + j + k + l + m + n + 1;
}
.file   "paramDemo2.c"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $80, %rsp
movl $1, -4(%rbp)


movl -4(%rbp), %r9d
movl -4(%rbp), %r8d
movl -4(%rbp), %ecx
movl -4(%rbp), %edx
movl -4(%rbp), %esi
movl -4(%rbp), %eax


movl -4(%rbp), %edi
movl %edi, 48(%rsp)

movl -4(%rbp), %edi
movl %edi, 40(%rsp)

movl -4(%rbp), %edi
movl %edi, 32(%rsp)

movl -4(%rbp), %edi
movl %edi, 24(%rsp)

movl -4(%rbp), %edi
movl %edi, 16(%rsp)

movl -4(%rbp), %edi
movl %edi, 8(%rsp)

movl -4(%rbp), %edi
movl %edi, (%rsp)

movl %eax, %edi
movl $0, %eax
call fun
movl $0, %eax
leave
ret
.size main, .-main
.globl fun
.type fun, @function
fun:
pushq %rbp ;开辟栈帧
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)

movl -8(%rbp), %eax
movl -4(%rbp), %edx
addl %eax, %edx

movl -12(%rbp), %eax
addl %eax, %edx

movl -16(%rbp), %eax
addl %eax, %edx

movl -20(%rbp), %eax
addl %eax, %edx

movl -24(%rbp), %eax
addl %eax, %edx

movl 16(%rbp), %eax
addl %eax, %edx

movl 24(%rbp), %eax
addl %eax, %edx

movl 32(%rbp), %eax
addl %eax, %edx

movl 40(%rbp), %eax
addl %eax, %edx

movl 48(%rbp), %eax
addl %eax, %edx

movl 56(%rbp), %eax
addl %eax, %edx

movl 64(%rbp), %eax
addl %edx, %eax

addl $1, %eax
movl %eax, -4(%rbp)
popq %rbp
ret

通过观察我们可以发现,当寄存器不够使用时,就会使用 寄存器 + 栈内存 的方式 进行传递参数。别人定义的,自己去实现的。

结论:
global表示全局的符号,(变量或者函数)
.section描述节信息
.data表示数据段
.text表示代码段

.code32编译32位的东西可以这么做

数据的可见性

#include <stdio.h>

int data = 0;

int sum(){
return data;
}

int main(){
sum();
return 1;
}

过将该代码编译,可以得到如下汇编代码

.file   "main.c"

.globl data
.bss
.align 4
.type data, @object
.size data, 4
data:
.zero 4


.text
.globl sum
.type sum, @function
sum:
pushq %rbp
movq %rsp, %rbp
movl data(%rip), %eax
popq %rbp
ret
.size sum, .-sum
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
call sum
movl $1, %eax
popq %rbp
ret

可见,在 ​​sum 函数​​​ 和 ​​data​​​ 前,都有一个 ​​.global​​​ ,我们就想是否是由于 ​​.global​​​ 导致了数据和函数的全局可见性呢?为了验证这一点,我们可以将 ​​int data​​ 定义到其他文件,然后将两个文件合并编译,查看是否可以编译成功

// demo.c 文件
#include <stdio.h>

int sum(){
return data;
}

int main(){
sum();
return 1;
}

// data.C 文件
int data = 0;

编译的时候会编译不通过,但是这并不是什么问题,是因为一些语法问题,虽然说编译是不涉及到代码之间的整合的,但是我们在之后运行的时候是需要知道怎么去找这个数据的,找一个数据需要怎么找呢? 通过​​ 变量名 + 类型​​​,这两者匹配就可以确定位置了,所以需要给定这两个信息,这就需要使用 ​​extern​​ 关键字了,用它来表明:该数据是在外部文件中定义的,包括 变量名信息 和 变量类型信息

// demo.c 文件
#include <stdio.h>
extern int data;
int sum(){
return data;
}

int main(){
sum();
return 1;
}

单独对该文件进行编译,得到汇编代码:

.file   "main.c"
.text
.globl sum
.type sum, @function
sum:
pushq %rbp
movq %rsp, %rbp
movl data(%rip), %eax
popq %rbp
ret
.size sum, .-sum
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
call sum
movl $1, %eax
popq %rbp
ret

指针

一个需求:在函数 A 中的一个变量,想要在调用函数 B 后,由 函数 B 将该值修改后返回,并且函数 B 对该值的修改需要对 函数 A 可见。

如果我们需要多个方法中共享一个数据的操作的话,就需要他们同时有该真实数据的内存地址,所以就需要进行内存地址的传递。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rXGY3Css-1670667358690)(C:/Doc/typora_pic/1653469335801-8f59b911-d6f5-4893-bec0-78da70749441-16662439264426.jpeg)]

首先我们需要传递数据的地址,就需要拿到数据的地址,在汇编语言层面,是通过​lea​​​指令获取到数据的地址,如:​​lea -10(%ESP)​​,而为了可读性和方便,C语言 中将该取地址操作抽象为了​&​,类似:​&a​标识获取a变量的地址。

既然我们拿到了内存地址,那需要用什么去表示当前值是一个内存地址值呢?-----> 在汇编层面呢,是使用了​()​来表示括号内的值是内存地址,通过​(内存地址)​获取其中的内容,例如:​mov -8(%EPB) %EAX​,同样为了方便对​()​抽象,成为​*​,类似:​void *p​

而需要一种类型来接受这个值,也就是指针,但是我们需要知道所指的这块儿内存中存放的数据的宽度,所以就需要借助原有类型来数据宽度,将表示宽度的类型放到​*​之前,表示宽度,如:​int *P​表示指向一块内存地址,且该内存地址中的数据宽度为 int 的宽度,也就是4字节。

有了上面的推理,我们看一下接下来这段代码

#include <stdio.h>
void incr(int *p){
(*p)++;
}

int main(){
int shareData = 1;
incr(&shareData);
return 1;
}

汇编得出以下汇编代码:

main:
pushq %rbp
movq %rsp, %rbp
// 开辟栈空间存放 数据
subq $16, %rsp
// 创建变量 shareData
movl $1, -4(%rbp)
// 取地址 放入到 rax 中
leaq -4(%rbp), %rax
// 将 shareData 的地址放入到 rdi 作为传参
movq %rax, %rdi
// 将 eax 归零
movl $0, %eax
call incr
movl $1, %eax
leave
ret


incr:
pushq %rbp
movq %rsp, %rbp
// 取参,此时拿到的是 地址 ==> p
movq %rdi, -8(%rbp)
// 将地址信息放入 rax
movq -8(%rbp), %rax


// 将 rax 中的地址 中 的数据 放入到 eax 中 ---> 1
movl (%rax), %eax
//
leal 1(%rax), %edx
// 将地址信息放入 rax 中
movq -8(%rbp), %rax
// 将运算的结果, 放入到 rax 所表示的地址中
movl %edx, (%rax)

popq %rbp
ret

为何这里实现 (*p)++ 是通过 ​​leal 1(%eax),%edx ​​?

通过该行代码的后续操作,可见此次往 edx 存放的内容是最终的运算结果 ----- (​​(*p)++​​​后的结果 2),但是已知​​lea​​指令是获取地址的行为呀,

猜想1 : 难道 lea 操作数 等价于 mov 指令?

验证:

  1. 更改汇编代码
incr:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
// 更改这个位置
movl 1(%rax), %edx
movq -8(%rbp), %rax
movl %edx, (%rax)
popq %rbp
ret
  1. 编译成功,但是执行报错 ----- 段错误

猜想2:既然我无法解决 leal 指令的问题,那么这个​​1(%rax)​​​是怎么计算的呢?因为 ​​()​​​是解引用,也即括号内的值是一个地址,但是此处的 rax 中装入的已经是一个数了,为何要解引用呢,难道 ​​立即数(数)​​表示 这个数 + 立即数 的结果?

验证:

  1. 更改汇编代码如下
incr:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
// 更改这个位置
leal 2, %edx
movq -8(%rbp), %rax
movl %edx, (%rax)
popq %rbp
ret
  1. 编译成功,且得到输出结果 2

所以目前可以认为 ​​leal num,reg​​​可以将该数存入寄存器中,且​​imm(num)​​可以表示 num + imm

论据:

intel 手册中只谈了 lea 指令将地址加载的作用,并不涉及到 lea 一个数

最终得知这是一个技巧

数组是什么

我们再看如下代码

#include <stdio.h>

int main(){
int arr[] = {1,2,3};
int *p = &arr[0];
int a = *P;
int b = *(p + 1);
int c = *(p + 2);
}

编译得到汇编代码如下:

main:
pushq %rbp
movq %rsp, %rbp
// 数组声明
movl $1, -32(%rbp)
movl $2, -28(%rbp)
movl $3, -24(%rbp)

// int *p = &arr[0]
// 将 arr[0] 地址放入到 p 中
leaq -32(%rbp), %rax
movq %rax, -8(%rbp)

// 将地址放入 rax
movq -8(%rbp), %rax
// 将 rax 中的地址 取出数据,放入 eax
movl (%rax), %eax
// 将 eax 中的数据 放入 栈中开辟的变量内存中
movl %eax, -12(%rbp)
// ……
movq -8(%rbp), %rax
movl 4(%rax), %eax
movl %eax, -16(%rbp)
movq -8(%rbp), %rax
movl 8(%rax), %eax
movl %eax, -20(%rbp)
movl $0, %eax
popq %rbp
ret

综上,我们发现通过指针其实是可以实现数组的,或者说指针和数组是一种实现方式,只不过数组在使用上更加方便 ----> 所以我们可以理解为 数组是指针的语法糖

结构体又是什么?

简单的结构体

#include <stdio.h>

struct Student{
int age;
}

int main(){
struct Student stu = {666};
printf("%s",stu.age);
}

如果有这样一个结构体,那么在内存中是怎样去存放数据的呢?

猜想一下:如果是在方法中的话,就会先开辟栈空间,开辟多少靠计算,类似上述代码就是:4字节,但是需要进行内存对齐(cpu规定),所以就应该是开辟 16字节空间

.file "demo.c"
.section .rodata
.LC0:
.string "%s"

.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp 开辟16字节栈空间
movl $666, -16(%rbp) 填充 age 属性,立即数$666放入到开辟的空间中

*****printf 函数******

movl -16(%rbp), %eax 通过eax寄存器交给esi寄存器(字符串操作时,用于存放数据源的地址)
movl %eax, %esi
movl $.LC0, %edi 字符串操作时,用于存放目的地址的,和esi两个经常搭配一起使用,执行字符串的复制等操作
movl $0, %eax
call printf
leave
ret

结构体 + 字符串

那在内存中是怎样去定位不同的内容的呢?很容易想到通过偏移量来定位,比如我想要得到 id 信息,就需要拿到该结构的初始地址,然后偏移到对应的位置即可。

如果是靠偏移量去做,那不就和数组一样了?只不过是内部数据的类型不统一而已,那我们是否可以拿到一个指针指向初始地址,然后 ++ 遍历获取值呢?其实这是不行的,虽然他类似于数组,但是本质上来说,这并不是数组。(当然如果真的这样操作了,由于C语言没有进行越界检查,还是可以执行成功的,只是结果不会是想见的那样)

struct Student{
int age;
char name[4];
}

int main() {
struct Student stu = {
666,"aaa"
};
printf("%s", stu.name);
}

执行:​​gcc -S -fno-asynchronous-unwind-tables demo.s​

.file "hello2.c"
.section .rodata 只读数据段
.LC0:
.string "%s"

.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp

subq $16, %rsp
movl $666, -16(%rbp) 存立即数
movl $6381921, -12(%rbp) 6381921是aaa的阿斯克码

leaq -16(%rbp), %rax 把地址给到rax寄存器
addq $4, %rax 通过rax寄存器将地址给到rsi寄存器
movq %rax, %rsi
movl $.LC0, %edi
movl $0, %eax
call printf
leave
ret

结论:

1、字符串采用ASCCII编码,放到自己的内存空间栈上
2、“aaa”取出,拼成32位,高八位和低八位依次组合

结构体 + 指针

#include<stdio.h>

struct Student{
int age;
char *name;
};

int main() {
struct Student stu = {
666,"aaa"
};
char name = stu.name[0];
printf("%s", stu.name);
return 1;
}
.file   "demo.c"

;***** 只读数据段 ******;
.section .rodata
.LC0:
.string "aaa"
.LC1:
.string "%s"

;***** main函数 ******;
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movl $13, -32(%rbp) ;在开辟的32位中分了4位给666 [666, , , ]
movq $.LC0, -24(%rbp) ;变成[13, ,.LC0首地址低 , .LC0首地址高 , ]
movq -24(%rbp), %rax
movzbl (%rax), %eax
movb %al, -1(%rbp) ;拿到低8位

;***** print函数 ******;
movq -24(%rbp), %rax
movq %rax, %rsi
movl $.LC1, %edi
movl $0, %eax
call printf

movl $1, %eax
leave
ret

数组不等于指针,指针不等于数组。直接声明的数组会在栈空间中生成,而声名指针会在rodata中生成。