本文主要是介绍从汇编和底层的角度看c和类c语言,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
从汇编和底层的角度看c和类c语言
写这篇文章的目的是对近期底层学习的总结,也算是勉励自己吧,毕竟是光靠兴趣苦逼自学不是自己专业的东西要承受很多压力。
https://blog.csdn.net/jggyyhh/article/details/50429886?utm_source=blogxg
要想深入理解C语言就不得不要知道几个知识点:
1.众所周知用任意一高级语言(不是脚本语言)写的代码都要经过类似:预处理->编译成汇编代码(compilation)->汇编(assembly)->连接(linking)这样的阶段。其中预处理产生.i文件,compilation产生.s文件,assembly产生.o文件,最后连接才会产生可执行文件,.o文件中不同机器上是不同的,而Java的能够“一次编译,到处运行“是因为Java不会像c那样在不同机器上产生不同的.o文件,而是用jvm虚拟机屏蔽了不同机器上的不同之处,于是只有不同的机子上都要Java的插件,一次编译后的文件就能到处运行。(可以想象的是为啥Android机的硬件配置往往要比iphone好,因为Android机正用了java的技术故中间多了一次转换过程当然效率要比用object-c编的iOS程序低,不过据说最近jvm采用了一些技术将效率提高了不小,不过这个我还没研究过就不说了)。
2.当你的代码被编译器编译成可执行文件(不一定是exe,这是个误区,以PE文件为例,这些格式其实是在PE文件头偏移量为0016h处的Characteristics字段表明的,如果是exe这一字段为0x0f01),不同的操作系统下的可执行文件是不同的,linux下为ELF,windows下为PE。由于我比较熟悉PE文件的格式,我就拿PE文件做个例子,你反汇编任意一个Windows的可执行文件你就会发现每个文件都被分成了很多个块,大致分成了.text,.idata,.rdata,.data,.rsrc块,这是为啥呢?这其实是为了方便程序映射到进程内存空间,因为为了方便管理和实现各种机制,进程的内存空间是分段的,在linux下一个进程的内存空间大致是这样:
其中进程的用户态的线性地址空间是从0x00000000到0xbfffffff,也就是一般的应用程序跑的线性地址空间(内存中每一个字节的数据被赋予一个地址),注意这里是线性地址空间, 你反汇编左侧的地址空间是逻辑地址,
如上图左侧的是逻辑地址,(这些地址都是16进制),逻辑地址要经过分段机制才能指向线性地址,而线性地址要经过分页才能指向物理地址(物理地址才是内存条上),(有些操作系统没有分段机制,逻辑地址等于线性地址)。这其中的细节展开是一章的内容,我就不多说,有兴趣的可以看下linux内核方面的书籍,你要清楚你的程序要跑起来必定cpu要为你的程序分配内存(其实还有很多东西),跑起来后看情况你的程序会以一个进程或者线程的状态出现在操作系统上(进程的描述可不是简单的pid就能标识的,而是task_struct这个被称为进程描述符的东西同样这些东西要参考内核方面的书籍)。下图是windows可执行文件的映射(比较懒啊,直接把笔记弄上去了):
我想经过我这样一番描述你大致模糊的清楚一个程序在你电脑的存在和运行是啥情况了,下面我将分析语句了
静态作用域:
没错,这就是《编译原理》里的那部分内容,不过我加上了我的从底层上的一些见解即解释,什么是静态作用域呢?通俗的说就是你通过源代码就能判断一个声明的作用范围,在
这个范围内所有对该声明变量的使用都指向那个声明。c语言的(类c语言)作用域规则是基于程序结构的(块),也就是和你的“{ }”符号的使用有关,如下图:
最后一个cout<<a<<b打印的a的值为1,因为在一个块(也是一个作用域)内的语句会首先使用该块内的声明,如B3域内cout<<a<<b打印的a的值是3,如果该块内没有这个声明,则
找到其父块,如B3域内cout<<a<<b打印的b的值是2。其实,int a是一种声明,int a=1也是声明,而定义是在你声明过后类似于 a=1这种语句,其实定义可以看成是定值,而a这个东西
只是一个名字,名字和变量(内存位置也即不同的内存地址)的关系如图:
在不同域名字可以一样,但是因为其环境(作用域)不同其实它指向的内存位置是不同的,且你在定义之前必须声明,要不然它不清楚是对哪个内存位置进行赋值操作,如:B2域和B3域都有个int a =*的声明,其实它们分别指向不同的内存位置所以可以存不同的值。故定义(定值)所指向的变量(实际上是内存位置)是取决于作用域的声明的,即使是相同语句(名字)也会随环境变化而赋予不同变量值。又因为C语言在作用域内是按顺序执行语句的,也就有了这个例子网上找的例子,其中int max(int,int);声明了个函数变量(有了一个相应的内存地址),它的作用域为整个程序,但是这个变量在这个作用域内还没有值,int main()函数下是另一个作用域,在这个作用域内并没有max函数的声明故它调用其父作用域的声明,在其父作用域内有个函数声明(因为在执行main函数前就执行了int max(int,int);),故函数成功调用,此例中你将int max(int,int);放在main()函数之下就不行了,这是因为C语言是顺序执行的,其实此例的最后7行既可以说是定义也可以说是声明,就像int a=1一样。
而java,c++与c语言的不同在于,它多了public,private,protected等等这些限制作用域的关键字,而不像c语言那样仅仅靠“{ }”程序员自己限制作用域范围或者函数(不同函数也是个不同作用域),与是乎java就有了许多个不同类型的被封装的作用域,比如说public声明的方法能被所有定义的类的对象调用。。。于是乎产生了对象这种东西,我认为c语言不是面向对象的语言的根本原因是它没有对作用域进行自定义化的封装,没有产生有独特性质方法(在c语言是函数)的“对象”,通俗上说是没有类似public这样的关键字。(只是个人见解,大牛见了不要见笑)
接下是重点了: 上面说了那么多其实都没深入到汇编层次也没用上之前我叙述的进程内存的知识,也没有从底层给出不同作用域的实现机制,接下来才是关键之处。
这是我简化后的linux进程内存存储方式(类似于第一张图,其实第一张图也是简略后的,.text段和.data段中有很多其它的东西(segement),毕竟你一个比较大的程序要有动态链接库还有一些则与linux中ANSI C的函数库libc的函数有关,这些东西要不涉及内核调用要不和库函数有关有的甚至与gcc编译器有关),线性地址从左到右依次变大,其中.text段中存有只读的二进制文件, .data 存有全局初始化变量如:static Int a=0 。.bss段存了全局未初始化变量如:static Int a。你会纳闷那我函数内存储的变量 如在B2中的int b=2,b所指向的变量存在哪呢?其实它们都存在stack这里面,stack也就是栈的意思,只要没有全局化声明的变量都存在stack里面。声明在static内的变量是固定的(地址固定),也即一旦你在程序里面改变了这个名字的值那么它会在你程序运行周期内永久改变,无论你改变的语句所在的作用域是啥。接下来我将通过反汇编一些程序为你揭示那些普通函数的变量是怎样在栈内存在的:(没学过汇编的朋友接下来的内容你可能会看不懂,但是没法,我的主题是深入理解c语言,不过这之上的内容我认为也是很有意义的)
首先在linux用 objdump -S test1.o 命令反汇编我之前编好的test.o的还未链接成可执行文件的.o文件(可用 gcc -c -o test1.c 命令产生,-S是将汇编代码和从语言同时显示),因为.o文件不是可执行文件没链接,故当在一个函数内调用另一个函数时没有call语句,.o文件链接后会产生很多不是源代码的段,这是系统自带的调用或者库链接甚至有些段是用来传递用户态进程的寄存器数据给内核的,这些机制的存在我认为不仅仅出于系统功能的作用,还有很大部分出于安全性,最早之前的栈分配方式是非常容易被缓冲区攻击的。之前的C语言程序反汇编的代码大致是这样的点此处查看,栈的保护方式多种多样,有的在返回地址处加垫片,有的用ASLR技术也就是所谓的地址空间随机化技术,在我分析完代码后会简单地演示这种技术,这些技术随着linux不断发展而更新,使得操作系统越来越安全,这就是开源的魅力,相当于全世界的高手都在参与操作系统的更新,这也是linux的魅力与活力。我接触过shellcode的编写,虽然依旧很菜,但是我还是大致知道常见的几种缓冲区攻击和一些过时的漏洞。好了,言归正传,由于我对现在版本内核的保护机制不了解,故有些语句作用我不太清楚,只能瞎猜测一番,如有大牛看到不要见笑,前面说到.o文件,我认为.o文件不太适合演示,故我演示的是反汇编可执行文件test1,用 gcc -o test test1.c命令编译,然后用objdump -d -M i386 test1 反汇编,这里的-M命令选项是指定汇编语言的格式,用objdump -i 可以看到格式选项,从语句形式的角度看一共有两种格式,intel和AT&T,默认的是AT&T,这两种格式差别不大,然后每种格式下分了32位和64位两种,其实是寄存器改变了,不过64位寄存器是兼容32位的,为了方便我将统一使用AT&T的32位(i386)指令集,你甚至可以加两个-M选项如objdump -d -M i386 -M intel test1 这使用的是 intel的32位指令集。64位寄存器和32位的兼容如下图:
源代码test1.c:
#include<stdio.h>
int sum(int temp1,int temp2);
int main()
{
int i;
i=sum(2,3);
return 0;
}
int sum(int temp1,int temp2)
{ int c=temp1;
int b=temp2;
int a= b+c;
return a;
}
在这我要纠正一个很多人都会犯的错误,就是写void main()这种形式的主函数,main()函数的返回值必须是int,linux进程退出分为正常退出和异常退出两种,正常退出中有一种是在main()函数里执行return操作(其它的是调用内核函数exit()和_exit(),其中exit()会将内存缓冲区数据回写给文件)main函数的返回值由 __libc_start_main接收,并传递给exit,return+非零值 表示非正常退出(另外进程中断时会调用about()函数表示非正常退出),其实c语言的return机制非常像Java中的try(),catch()的异常抛出,然而我们大多数人把return当做返回值用,其实从另一角度这也可以看做是“异常”处理吧,如果在main()函数(或者其它函数)里调用其它函数,当被调用函数内的return执行完后会将控制权(看后面就知道是cpu指令寄存器eip(rip))交给调用函数,如果main()函数中执行完return则将控制权交给操作系统,在早期的编译器版本中void main()会报错,新版本的编译器会在void main()中自动加入return 0。这个错误看似没啥,但是搞不好会被技艺高超的黑客所利用。
好回归正题:反汇编代码:我主要关心源代码的函数main和sum,因为其它段是和源代码内容基本无关的(其实是有些东西太复杂不懂)
test1: file format elf64-x86-64
Disassembly of section .init:
0000000000400370 <_init>: /这个区和最后的_fini和gcc编译器在链接时加载一般init与内核调用有关,这个我们
不太关心(其实我也没深入研究过<img alt="害羞" src="http://static.blog.csdn.net/xheditor/xheditor_emot/default/shy.gif" />不懂,大概有个模糊概念)
400370: 48 dec %eax
400371: 83 ec 08 sub $0x8,%esp
400374: 48 dec %eax
400375: 8b 05 45 05 20 00 mov 0x200545,%eax
40037b: 48 dec %eax
40037c: 85 c0 test %eax,%eax
40037e: 74 05 je 400385 <_init+0x15>
400380: e8 2b 00 00 00 call 4003b0 <__gmon_start__@plt>
400385: 48 dec %eax
400386: 83 c4 08 add $0x8,%esp
400389: c3 ret
Disassembly of section .plt:
0000000000400390 <__libc_start_main@plt-0x10>:
400390: ff 35 3a 05 20 00 pushl 0x20053a
400396: ff 25 3c 05 20 00 jmp *0x20053c
40039c: 0f 1f 40 00 nopl 0x0(%eax)
00000000004003a0 <__libc_start_main@plt>:
4003a0: ff 25 3a 05 20 00 jmp *0x20053a
4003a6: 68 00 00 00 00 push $0x0
4003ab: e9 e0 ff ff ff jmp 400390 <_init+0x20>
00000000004003b0 <__gmon_start__@plt>:
4003b0: ff 25 32 05 20 00 jmp *0x200532
4003b6: 68 01 00 00 00 push $0x1
4003bb: e9 d0 ff ff ff jmp 400390 <_init+0x20>
Disassembly of section .text:
00000000004003c0 <_start>: /这是真正的程序入口处
4003c0: 31 ed xor %ebp,%ebp
4003c2: 49 dec %ecx
4003c3: 89 d1 mov %edx,%ecx
4003c5: 5e pop %esi
4003c6: 48 dec %eax
4003c7: 89 e2 mov %esp,%edx
4003c9: 48 dec %eax
4003ca: 83 e4 f0 and $0xfffffff0,%esp/看到这个语句我想到了垫片保护栈的技术
但是好像有点不太一样,这处语句附近一定保存了argc 和argv[].
4003cd: 50 push %eax
4003ce: 54 push %esp
4003cf: 49 dec %ecx
4003d0: c7 c0 70 05 40 00 mov $0x400570,%eax
4003d6: 48 dec %eax
4003d7: c7 c1 00 05 40 00 mov $0x400500,%ecx
4003dd: 48 dec %eax
4003de: c7 c7 b6 04 40 00 mov $0x4004b6,%edi
4003e4: e8 b7 ff ff ff call 4003a0 <__libc_start_main@plt>
4003e9: f4 hlt
4003ea: 66 0f 1f 44 00 00 nopw 0x0(%eax,%eax,1)
00000000004003f0 <deregister_tm_clones>:
4003f0: b8 07 09 60 00 mov $0x600907,%eax
4003f5: 55 push %ebp
4003f6: 48 dec %eax
4003f7: 2d 00 09 60 00 sub $0x600900,%eax
4003fc: 48 dec %eax
4003fd: 83 f8 0e cmp $0xe,%eax
400400: 48 dec %eax
400401: 89 e5 mov %esp,%ebp
400403: 76 1b jbe 400420 <deregister_tm_clones+0x30>
400405: b8 00 00 00 00 mov $0x0,%eax
40040a: 48 dec %eax
40040b: 85 c0 test %eax,%eax
40040d: 74 11 je 400420 <deregister_tm_clones+0x30>
40040f: 5d pop %ebp
400410: bf 00 09 60 00 mov $0x600900,%edi
400415: ff e0 jmp *%eax
400417: 66 0f 1f 84 00 00 00 nopw 0x0(%eax,%eax,1)
40041e: 00 00
400420: 5d pop %ebp
400421: c3 ret
400422: 66 66 66 66 66 2e 0f data16 data16 data16 data16 nopw %cs:0x0(%eax,%eax,1)
400429: 1f 84 00 00 00 00 00
0000000000400430 <register_tm_clones>: /从字面上看这与将寄存器复制到内核有关
400430: be 00 09 60 00 mov $0x600900,%esi
400435: 55 push %ebp
400436: 48 dec %eax
400437: 81 ee 00 09 60 00 sub $0x600900,%esi
40043d: 48 dec %eax
40043e: c1 fe 03 sar $0x3,%esi
400441: 48 dec %eax
400442: 89 e5 mov %esp,%ebp
400444: 48 dec %eax
400445: 89 f0 mov %esi,%eax
400447: 48 dec %eax
400448: c1 e8 3f shr $0x3f,%eax
40044b: 48 dec %eax
40044c: 01 c6 add %eax,%esi
40044e: 48 dec %eax
40044f: d1 fe sar %esi
400451: 74 15 je 400468 <register_tm_clones+0x38>
400453: b8 00 00 00 00 mov $0x0,%eax
400458: 48 dec %eax
400459: 85 c0 test %eax,%eax
40045b: 74 0b je 400468 <register_tm_clones+0x38>
40045d: 5d pop %ebp
40045e: bf 00 09 60 00 mov $0x600900,%edi
400463: ff e0 jmp *%eax
400465: 0f 1f 00 nopl (%eax)
400468: 5d pop %ebp
400469: c3 ret
40046a: 66 0f 1f 44 00 00 nopw 0x0(%eax,%eax,1)
0000000000400470 <__do_global_dtors_aux>:
400470: 80 3d 89 04 20 00 00 cmpb $0x0,0x200489
400477: 75 11 jne 40048a <__do_global_dtors_aux+0x1a>
400479: 55 push %ebp
40047a: 48 dec %eax
40047b: 89 e5 mov %esp,%ebp
40047d: e8 6e ff ff ff call 4003f0 <deregister_tm_clones>
400482: 5d pop %ebp
400483: c6 05 76 04 20 00 01 movb $0x1,0x200476
40048a: f3 c3 repz ret
40048c: 0f 1f 40 00 nopl 0x0(%eax)
0000000000400490 <frame_dummy>:
400490: bf e8 06 60 00 mov $0x6006e8,%edi
400495: 48 dec %eax
400496: 83 3f 00 cmpl $0x0,(%edi)
400499: 75 05 jne 4004a0 <frame_dummy+0x10>
40049b: eb 93 jmp 400430 <register_tm_clones>
40049d: 0f 1f 00 nopl (%eax)
4004a0: b8 00 00 00 00 mov $0x0,%eax
4004a5: 48 dec %eax
4004a6: 85 c0 test %eax,%eax
4004a8: 74 f1 je 40049b <frame_dummy+0xb>
4004aa: 55 push %ebp
4004ab: 48 dec %eax
4004ac: 89 e5 mov %esp,%ebp
4004ae: ff d0 call *%eax
4004b0: 5d pop %ebp
4004b1: e9 7a ff ff ff jmp 400430 <register_tm_clones>
00000000004004b6 <main>:
4004b6: 55 push %ebp //保存原栈底,然后栈顶指针(esp)向上移动4位,因为
ebp是32位寄存器(32bit)即4个字节(8bit),内存给每个字节分配一个地址。
4004b7: 48 dec %eax //这句的eax自减1,和下下句我完全不知道是啥意思,我
猜测是某种计数用的。
4004b8: 89 e5 mov %esp,%ebp//创建新栈底
4004ba: 48 dec %eax
4004bb: 83 ec 10 sub $0x10,%esp//esp向上偏移16位,因为$0x10是16进制,开辟了个
能存4个整型(int)数据的区域或者16个char数据类型的区域
4004be: be 03 00 00 00 mov $0x3,%esi//将第二个参数值3存入esi
4004c3: bf 02 00 00 00 mov $0x2,%edi//将第一个参数值2存入esi
4004c8: e8 0a 00 00 00 call 4004d7 <sum>/将jmp 4004d7即跳到sum函数作用域,然后将下一
条语句的地址作为返回地址压栈
4004cd: 89 45 fc mov %eax,-0x4(%ebp)//将从sum那计算出的a值存到ebp的上面第一个
整型区域(偏移量4).这也就是i的内存空间
4004d0: b8 00 00 00 00 mov $0x0,%eax//清空eax
4004d5: c9 leave //相当于 mov %esp,%ebp pop %ebp两条语句,目的是还原原栈底
4004d6: c3 ret //相当于 pop eip,作用是返回调用该函数的函数的空间,作用
域被改变
00000000004004d7 <sum>:
4004d7: 55 push %ebp//保存原栈底,然后栈顶指针(esp)向上移动4位,因为
ebp是32位寄存器(32bit)即4个字节(8bit),内存给每个字节分配一个地址。
4004d8: 48 dec %eax
4004d9: 89 e5 mov %esp,%ebp//同main
4004db: 89 7d ec mov %edi,-0x14(%ebp)//将第一个参数值2从esi存入ebp上方偏移量为
14的内存空间处
4004de: 89 75 e8 mov %esi,-0x18(%ebp)//将第2个参数值3从esi存入ebp上方偏移量为
14的内存空间处
4004e1: 8b 45 ec mov -0x14(%ebp),%eax//将第一个参数值2从存入eax
4004e4: 89 45 fc mov %eax,-0x4(%ebp)//将eax的值2从存入ebp上方偏移量为
4的内存空间处,相当于sum函数内的c指向的空间位置
4004e7: 8b 45 e8 mov -0x18(%ebp),%eax//将第二个参数值3从存入eax
4004ea: 89 45 f8 mov %eax,-0x8(%ebp)//将eax的值3从存入ebp上方偏移量为
8的内存空间处,相当于sum函数内的b指向的空间位置
4004ed: 8b 55 f8 mov -0x8(%ebp),%edx
4004f0: 8b 45 fc mov -0x4(%ebp),%eax
4004f3: 01 d0 add %edx,%eax//这上面3句相当于函数内的a=b+c,且a的值存入eax中
4004f5: 89 45 f4 mov %eax,-0xc(%ebp)//将eax的值a(5)从存入ebp上方偏移量为
12的内存空间处,相当于sum函数内的a指向的空间位置
4004f8: 8b 45 f4 mov -0xc(%ebp),%eax//将a的值存入eax,为之后main函数传值做铺垫
4004fb: 5d pop %ebp//恢复原栈底
4004fc: c3 ret /同main
4004fd: 0f 1f 00 nopl (%eax) //占位用,无实际意义
0000000000400500 <__libc_csu_init>:
400500: 41 inc %ecx
400501: 57 push %edi
400502: 41 inc %ecx
400503: 89 ff mov %edi,%edi
400505: 41 inc %ecx
400506: 56 push %esi
400507: 49 dec %ecx
400508: 89 f6 mov %esi,%esi
40050a: 41 inc %ecx
40050b: 55 push %ebp
40050c: 49 dec %ecx
40050d: 89 d5 mov %edx,%ebp
40050f: 41 inc %ecx
400510: 54 push %esp
400511: 4c dec %esp
400512: 8d 25 c0 01 20 00 lea 0x2001c0,%esp
400518: 55 push %ebp
400519: 48 dec %eax
40051a: 8d 2d c0 01 20 00 lea 0x2001c0,%ebp
400520: 53 push %ebx
400521: 4c dec %esp
400522: 29 e5 sub %esp,%ebp
400524: 31 db xor %ebx,%ebx
400526: 48 dec %eax
400527: c1 fd 03 sar $0x3,%ebp
40052a: 48 dec %eax
40052b: 83 ec 08 sub $0x8,%esp
40052e: e8 3d fe ff ff call 400370 <_init>
400533: 48 dec %eax
400534: 85 ed test %ebp,%ebp
400536: 74 1e je 400556 <__libc_csu_init+0x56>
400538: 0f 1f 84 00 00 00 00 nopl 0x0(%eax,%eax,1)
40053f: 00
400540: 4c dec %esp
400541: 89 ea mov %ebp,%edx
400543: 4c dec %esp
400544: 89 f6 mov %esi,%esi
400546: 44 inc %esp
400547: 89 ff mov %edi,%edi
400549: 41 inc %ecx
40054a: ff 14 dc call *(%esp,%ebx,8)
40054d: 48 dec %eax
40054e: 83 c3 01 add $0x1,%ebx
400551: 48 dec %eax
400552: 39 eb cmp %ebp,%ebx
400554: 75 ea jne 400540 <__libc_csu_init+0x40>
400556: 48 dec %eax
400557: 83 c4 08 add $0x8,%esp
40055a: 5b pop %ebx
40055b: 5d pop %ebp
40055c: 41 inc %ecx
40055d: 5c pop %esp
40055e: 41 inc %ecx
40055f: 5d pop %ebp
400560: 41 inc %ecx
400561: 5e pop %esi
400562: 41 inc %ecx
400563: 5f pop %edi
400564: c3 ret
400565: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%eax,%eax,1)
40056c: 00 00 00 00
0000000000400570 <__libc_csu_fini>:
400570: f3 c3 repz ret
Disassembly of section .fini:
0000000000400574 <_fini>:
400574: 48 dec %eax
400575: 83 ec 08 sub $0x8,%esp
400578: 48 dec %eax
400579: 83 c4 08 add $0x8,%esp
40057c: c3 ret
从源代码的分析可知,每一个函数的作用域相当于一个创建的栈,其内部的变量存入各自的栈中,调用一个函数只是将eip即cpu指令寄存器的值指向被调用函数的内存空间的开头然后将该调用语句的下一句地址压栈,且调用函数的栈顶是被调用函数的栈底,图类似于我上面给的超链接处的内容的图,只不过现在的内核为了安全舍弃了将参数压栈的这种调用方式而是通过寄存器esi,edi传递,想想也是,这样做防止了以前用类似strcpy()函数的缺陷而造成的缓冲区溢出。
下面我将演示ASLR技术也就是所谓的地址空间随机化技术,这个技术能干扰黑客编写shellcode,因为很多shellcode的编写要准确计算出esp到ebp之间的长度,然后用恶意代码填充,这我就不说方法了,找一本国外的黑客书籍都有介绍,但是基本上是没有的,就如我之前所说现在的linux内核版本加入了很多保护机制,你必须要绕开它你的shellcode才有用。
回到正题:这是我编的一段小代码,作用是打印esp寄存器的内容即栈顶地址
test.c源代码:
#include<stdio.h>
unsigned int get_esp(){
__asm__("movl %esp, %eax");
}
int main(){
printf("STACK ESP:0x%x\n", get_esp());
}
运行结果如图:
你会发现每运行一次,esp的值都有变化。
使用命令 echo "0" > /proc/sys/kernel/randomize_va_space #on slackware systems
将这个保护选项关闭后再运行,结果如下:
现在栈顶地址固定了,一般我研究shellcode时会把栈顶值固定,这样才容易出效果,毕竟比较菜。
这篇关于从汇编和底层的角度看c和类c语言的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!