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

C语言与汇编

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

image-20221210172857771

本书余下的篇幅讨论源代码文件中的内容;本节讨论创建源代码文 件的技巧。有些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中生成。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willorn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值