从C源代码到ELF可执行文件的生成过程
由C语言代码生成汇编语言
由汇编代码生成机器码
将多个机器码的目标链接成一个可执行文件
Section称为节,是指在汇编源码中经由关键字section或segment修饰、逻辑划分的指令或数据区域,汇编器
会将这两个关键字修饰的区域在目标文件中编译成节,也就是说"节"最初诞生于目标文件
中。
Segment称为段,是链接器根据目标文件中属性相同的多个Section合并后的Section集合
,这个集合称为Segment,也就是段,链接器把目标文件链接成可执行文件,因此段最终诞生于可执行文件
中。我们平时所说的可执行程序内存空间中的代码段
和数据段
就是指的Segment。
代码段(.text)是可执行指令的集合;数据段(.data)和 BSS 段(.bss)是数据的集合,其中.data 表示已经初始化的数据,.bss 表示未初始化的数据。
从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是 .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间。
一个可执行程序至少包含:代码段 + 数据段 + BSS 段
一般情况下,一个可执行二进制程序(在 linux 下为一个进程单元),在存储
时(没有加载到内存运行),至少拥有三个部分,分别是代码段(text)、数据段(data)、和BSS 段。
这三个部分一起组成了可执行程序(可能还有其他的段,和平台相关)
当应用程序运行
时(运行态),此时需要另外两个域:堆和栈。正在运行的程序:代码段 + 数据段 + BSS 段 + 堆 + 栈。
段名 | 存储属性 | 内存分配 |
---|---|---|
代码段 .text | 存放可执行程序的指令,存储态和运行态都有 | 静态 |
数据段 .data | 存放已初始化(非零初始化的全局变量和静态局部变量)的数据,存储态和运行态都有 | 静态 |
bss段 .bss | 存放未初始化(未初始化或者0初始化的全局变量和静态局部变量)的数据,存储态和运行态都有 | 静态 |
堆 heap | 动态分配内存,需要通过malloc手动申请,free手动释放,适合大块内存。容易造成内存泄漏和内存碎片。运行态才有。 | 动态 |
栈 stack | 存放函数局部变量和参数以及返回值,函数返回后,由操作系统立即回收。栈空间不大,使用不当容易栈溢出。运行态才有 | 静态 |
局部变量&全局变量
1 局部变量:“在函数
内定义的变量”,
即在一个函数内部定义的变量,只在本函数范围内
有效。
2 全局变量:“在函数
外定义的变量”,
即从定义变量的位置到本源文件结束
都有效。
目的:增加函数间数据联系的渠道。由于同一文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,
就能影响到其他函数中全局变量的值,相当于各个函数之间有直接的传递渠道。
为了便于区别两者,C程序设计人员有一个习惯,将全局变量名的第一个字母用大写表示。如:float Max=0,Min=0; //定义全局变量Max,Min
内部函数&外部函数
1.内部函数:只被本文件中其他函数
所调用。
定义内部函数时,在函数名、函数类型前加 static 。 static 类型名 函数名(形参表); 例如,static int fun(int a,int b);
2.外部函数:可供其他文件
调用。
定义外部函数时,在函数首部左端加 extern。extern int fun(int a,int b);
若在定义函数时省略extern,则默认为外部函数。
局部变量
普通的局部变量定义的时候直接定义或者在前面加上auto
void func1(void) { int i = 1; i++; printf("i = %d.\n", i); }
局部变量i的解析:
在连续三次调用func1中,每次调用时,在进入函数func1后都会创造一个新的变量i,
并且给它赋初值1,然后i++时加到2,
然后printf
输出时输出2.然后func1本次调用结束,
结束时同时杀死本次创造的这个i。这就是局部变量i的整个生命周期。
下次再调用该函数func1时,又会重新创造一个i,经历整个程序运算,
最终在函数运行完退出时再次被杀死。
静态局部变量
静态局部变量(static) 静态局部变量定义时前面加static关键字。
总结:
1、静态局部变量和普通局部变量不同。静态局部变量也是定义在函数内部
的,静态局部变量定义时前面要加static关键字来标识,静态局部变量所在的函数在多调用多次时,只有第一次才经历变量定义和初始化,以后多次在调用时不再定义和初始化,而是维持之前上一次调用时执行后这个变量的值。本次接着来使用。
2、静态局部变量在第一次函数被调用时创造并初始化,但在函数退出时它不死亡,而是保持其值等待函数下一次被调用。下次调用时不再重新创造和初始化该变量,而是直接用上一次留下的值为基础来进行操作。
3、静态局部变量的这种特性,和全局变量非常类似。它们的相同点是都创造和初始化一次,以后调用时值保持上次的不变。不同点在于作用域不同
全局变量
定义在函数外面的变量,就叫全局变量。
普通全局变量 普通全局变量就是平时使用的,定义前不加任何修饰词。普通全局变量可以在各个文件中使 用,可以在项目内别的.c文件中被看到,所以要确保不能重名。
静态全局变量 静态全局变量就是用来解决重名问题的。静态全局变量定义时在定义前加static关键字, 告诉编译器这个变量只在当前本文件内
使用,在别的文件中绝对不会使用。这样就不用担心重名问题。所以静态的全局变量就用在我定义这个全局变量并不是为了给别的文件使用,本来就是给我这个文件自己使用的。
跨文件引用全局变量(extern) 就是说,你在一个程序的多个.c源文件中,可以在一个.c文件中定义全局变量g_a,并且可以在别的另一个.c文件中引用该变量g_a(引用前要声明)
函数和全局变量在C语言中可以跨文件引用,也就是说他们的连接范围是全局的,具有文件连接属性,总之意思就是全局变量和函数是可以跨文件看到的(直接影响就是,我在a.c和b.c中各自定义了一个函数func,名字相同但是内容不同,编译报错。)。
局部变量和全局变量的对比:
1、定义同时没有初始化,则局部变量的值是随机的,而全局变量的值是默认为0.
2、使用范围上:全局变量具有文件作用域,而局部变量只有代码块作用域。
3、生命周期上:全局变量是在程序开始运行之前的初始化阶段就诞生,到整个程序结束退出的时候才死亡;而局部变量在进入局部变量所在的代码块时诞生,在该代码块退出的时候死亡。
4、变量分配位置:全局变量分配在数据段上,而局部变量分配在栈上
(具体解释在上方表格中有)
text段:也叫代码段,比如以上实例C语言源代码,存储sum,main,'Hello world!'
data段:存放已初始化(非零初始化的全局变量和静态局部变量)的数据,这里也就是str
Bss段:存放未初始化(未初始化或者0初始化的全局变量和静态局部变量)的数据,这里也就是glb
Stack:存放函数局部变量和参数以及返回值,函数返回后,由操作系统立即回收,这里的局部变量就是t,ptr(局部变量全部存放在栈里)
Heap:动态分配内存,需要通过malloc手动申请,这里通过malloc申请0x100的内存,我这里如果输入deadbeef,会存储在heap
x,y属于形参,在32位架构的情况下,要调用sum函数,在创建main函数的sum()函数的栈之前,会先将1 ,2压栈(也就是把1,2存放进栈)
如果是64架构的情况下,x,y不出现在虚拟内存空间中,会放入寄存器中
[]等同于C语言中的&
所有数据由低地址向高地址增长
栈相反,由高地址向低地址增长
Heap由低地址向高地址增长
通用数据传送指令
1.MOV(Move) 传送指令
MOV指令是数据传送指令,也是最基本的编程指令,用于将一个数据从源地址
传送到目标地址
(寄存器间的数据传送本质上也是一样的)。其特点是不破坏源地址单元的内容。
例如:
MOV AX,2000H;将16位数据2000H传送到AX寄存器
MOV AL,20H;将8位数据20H传送到AL寄存器
MOV AX,BX;将BX寄存器的16位数据传送到AX寄存器
MOV AL,[2000H];将2000H单元的内容传送到AL寄存器
需要注意的是:
(1)两个存储单元之间不能直接传送数据,即:MOV指令只允许一个操作数在存储器中。MOV [SI],[2000H];这是错误的
(2)MOV指令中立即数不能直接传送给段寄存器(CS、DS、SS、ES)和IP;段寄存器之间不能直接传送。MOV IP,2000 H ;这是错误的
(3)CS和IP不能作为目的操作数。MOV CS,AX ;这是错误的
(4)MOV指令中立即数不能作目标操作数。MOV 2000H,[SI] ;这是错误的
2.PUSH(Push onto the stack)进栈
push寄存器:将一个寄存器中的数据入栈
3.POP(Pop from the stack)出栈
pop寄存器:出栈用一个寄存器接收数据
4.XCHG(Exchange) 交换
是寄存器
和寄存器
之间,寄存器
和内存变量
之间内容的交换指令,两个操作数的数据类型要相同,可以是一个字节,也可以是一个字,也可以是双字。
累加器专用指令
1.IN(Input) 输入
2.OUT(Output)输出
CPU对外设的操作通过专门的端口读写指令来完成;
读端口用IN指令,写端口用OUT指令。
例子如下:
IN AL,21H;表示从21H端口读取一字节数据到AL
IN AX,21H;表示从端口地址21H读取1字节数据到AL,从端口地址22H读取1字节到AH
MOV DX,379H
IN AL,DX ;从端口379H读取1字节到AL
OUT 21H,AL;将AL的值写入21H端口
OUT 21H,AX;将AX的值写入端口地址21H开始的连续两个字节。(port[21H]=AL,port[22h]=AH)
MOV DX,378H
OUT DX,AX ;将AH和AL分别写入端口379H和378H
3.XLAT(Translate) 换码
在汇编中XLAT指令的作用就是在bx保存array的首地址,AL保存array的位置的基础上,执行XLAT指令,将返回值送入AL指令中.
指令如下指令后AL=5
lea bx,array
mov al,1
xlat ;返回值AL=array[AL]
有效地址送寄存器指令
1.LEA(Load effective address)有效地址送寄存器
2.LDS(Load DS with Pointer)指针送寄存器和DS
3.LES(Load ES with Pointer)指针送寄存器和ES
mov 和 lea 的区别
mov ecx,[eax+0x30]表示先运算eax+0x30得到一个结果,以这个结果为地址找一个ecx长度的内存数赋给ecx
lea ecx,[eax+0x30]表示先运算eax+0x30得到一个结果,把这个结果(mov时地址)赋给ecx
标志寄存器传送指令
1.LAHF(Load AH with flags) 标志送AH
2.SAHF(Store AH with flags)AH送标志寄存器
3.PUSHF(Push the flags) 标志进栈
4.POPF(Pop the flags)标志出栈
加法指令
1.ADD 加法
2.ADC(add with carry)带进位加法
3.INC(Increment) 加1
减法指令
1.SUB(Subtract)减法
2.SBB(Subtract with borrow)带借位减法
3.DEC(Decrement)减1
4.NEG(Negate)求补
5.CMP(Compare)比较
乘法指令
1.MUL(Unsigned Multiple) 无符号乘法
2.IMUL(Signed Multiple)带符号乘法
除法指令
1.DIV(Unsigned Divide)无符号除法
2.IDIV(Singed Divide)带符号除法
3.CBW(Convert byte to word)字节转换为字
4.CWD(Convert word to double word)字转换为双字
逻辑指令
1.AND 逻辑与
2.OR 逻辑或
3.NOT 逻辑非
4.XOR 异或
5.TEST 测试
TEST AX,BX 与 AND AX,BX 命令有相同效果,只是Test指令不改变AX和BX的内容,而AND指令会把结果保存到AX中。
Test命令将两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位。但是,Test命令的两个操作数不会被改变。运算结果在设置过相关标记位后会被丢弃。
移位指令
1.SHL(Shift logical left)逻辑左移
2.SAL(Shift arithmetic left)算术左移
3.SHR(Shift logical right) 逻辑右移
4.SAR(Shift arithmetic right)算术右移
5.ROL(Rotate left) 循环左移
6.ROR(Rotate right)循环右移
7.RCL(Rotate left through carry)带进位循环左移
8.RCR(Rotate right through carry)带进位循环右移
EAX:**累加器(Accumulator),** 它的低16位即是AX,而AX又可分为高8位AH和低8位AL。EAX是很多加法乘法的缺省寄存器,存放函数的返回值,用累加器进行的操作可能需要更少时间,在80386及其以上的微处理器中可以用来存放存储单元的偏移地址。AX寄存器是算术运算的主要寄存器。
EBX:基地址寄存器(Base Register), 它的低16位即是BX,而BX又可分为高8位BH和低8位BL。主要用于在内存寻址时存放基地址。
ECX:**计数寄存器(Count Register)**,它的低16位即是CX,而CX又可分为高8位CH和低8位CL。在循环和字符串操作时,要用它来控制循环次数;在位操作 中,当移多位时,要用CL来指明移位的位数;是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX:数据寄存器(Data Register),它的低16位即是DX,而DX又可分为高8位DH和低8位DL。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址;且总是被用来放整数除法产生的余数。
ESI/EDI:分别叫做源/目标索引寄存器(Source/Destination Index Register),它们的低16位分别是SI、DI。它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串。此外,它们又作为通用寄存器可以进行任意的常规的操作,如加减移位或普通的内存间接寻址。
EBP/BSP:分别是基址针寄存器(Base Pointer Register)
/堆栈指针寄存器(Stack Pointer Register)
,低16位是BP、SP,其内存分别放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
/底部
。主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。并且规定:BP为基指针(Base Pointer)寄存器,用它可直接存取堆栈中的数据;SP为堆栈指针(Stack Pointer)寄存器,用它只可访问栈顶。在32位平台上,ESP每次减少4字节。EBP最经常被用作高级语言函数调用的"框架指针"(frame pointer),EBP 构成了函数的一个框架,在C++反汇编中EBP通常是局部变量、传进来的参数。这里要注意在intel系统中栈是向下生长的(栈越扩大其值越小,堆恰好相反)。在通常情况下ESP是可变的,随着栈的生长而逐渐变小,而ESB寄存器是固定的,只有当函数的调用后,发生入栈操作而改变,在函数执行结束之后需要还原。