Linux下一个最简单的不依赖第三库的的C程序(2)
一个最简单的C程序,如下:
main.c:
int main()
{
char *str = "Hello World";
return ;
}
在64位平台上编译一个32位的程序,如下:(32位只是为了演示方便)
gcc -m32 -o hello main.c
./hello
echo $?
运行后 会看到结果是 8,说明程序正常。$?表示查看上一个命令的返回值
统计一下程序大小:7263字节,
wc -c hello
hello
通过 ldd 查看动态库的依赖,发现如下的一些动态库依赖,libc.so,可以说是几乎所有Linux上的程序都会依赖的一个基础C函数库。 ld-linux.so 是一个动态dll的加载库,它会动态加载libc.so, dll在Linux上叫 shared object, 后缀是 .so。 linux-gate.so是一个伪文件,它是与内核联系的一个接口,64位程序的linux-vdso.so也是同样的作用。
ldd hello
linux-gate.so. => (0xf7780000)
libc.so. => /lib/i386-linux-gnu/libc.so. (0xf75ae000)
/lib/ld-linux.so. (0x5662a000)
我们加上 -nostdlib 选项,该选项在linking链接阶段,不会把标准库和启动文件引入进来,下面提示没有找到_start这个函数符号,默认给了一个入口地址。
我们不管这个警告,执行程序,发现报段错误,如果统计这个可执行文件的大小,会发现大小有1128字节,比之前的7236小了好多。
gcc -m32 -nostdlib -o hello main.c
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000080480d8
./hello
Segmentation fault
ldd hello
not a dynamic executable
wc -c hello
hello
当使用 -nostdlib 编译的时候,提示没有找到_start函数符号,那我们会不会想到是glibc里面提供的某个函数呢?
Linux上的C程序,入口实际是_start,而不是我们代码里的main, _start定义在ctr1.o这个可重定位目标文件里
查找一下 ctr1.o 这个文件, 找到文件位置。
sudo find / -name "crt1.o"
只编译,不链接,-c表示只编译,生成一个32位的hello.o的目标文件
gcc -m32 -Os -c main.c -o hello.o
进行链接:
ld /usr/lib/i386-linux-gnu/crt1.o -o hello hello.o
#64位crt1.o 在/usr/lib/x86_64-linux-gnu/crt1.o
发现报错:
/usr/lib/i386-linux-gnu/crt1.o: In function `_start':
(.text+0xc): undefined reference to `__libc_csu_fini'
/usr/lib/i386-linux-gnu/crt1.o: In function `_start':
(.text+0x11): undefined reference to `__libc_csu_init'
/usr/lib/i386-linux-gnu/crt1.o: In function `_start':
(.text+0x1d): undefined reference to `__libc_start_main'
crt1.o对应的代码在glibc的源代码,在glibc-2.19 源代码目录下的 sysdeps/i386/start.S 或 sysdeps/x86_64/start.S,
_start 调用了__libc_start_main 等函数,__libc_start_main在csu/libc-start.c ,可以看到:
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
这就是main被调用的地方。这个是我们用户代码开始的地方
所以,在_start 和 main 之间的那些函数都是做一些glibc的准备工作,我们这里要剥离glibc的依赖,所以,我们直接可以这样做:
增加一个文件:
stubstart.s
.globle _start
_start:call main
编译:
gcc -m32 -nostdlib -Os stubstart.s main.c -o hello
./hello #运行报错:Segmentation fault
让我使用加上调试信息,重新编译,用gdb来看看
gcc -m32 -g -nostdlib -O0 stubstart.s main.c -o hello
首先反汇编看看,不同的编译优化,汇编代码会有一些不同:
objdump -d hello hello: file format elf32-i386
Disassembly of section .text:
080480d8 <_start>:
80480d8: e8 call 80480dd <main> #等价于:push %eip ; mov 80480dd, %eip 080480dd <main>:
80480dd: push %ebp
80480de: e5 mov %esp,%ebp #这2句是所有函数调用都有的,序言,套路,重新设置栈顶
80480e0: ec sub $0x10,%esp #分配临时变量
80480e3: c7 fc f1 movl $0x80480f1,-0x4(%ebp) #字符串赋值
80480ea: b8 mov $0x8,%eax #这里是把8赋值给eax,如果是把0赋值给eax,指令可能是:xor %eax,%eax 。
# xor 异或自己就是0,与 movl $, %eax 具有类似效果
80480ef: c9 leave #等价于:mov %ebp,%esp ; pop %esp ;
# 同时esp自动加一个步进(往栈基移动一个步进),
# 即80480dd地址后的2个指令反向执行,回归原位
80480f0: c3 ret #等价于 pop %eip,是call指令的反向执行,回归原位
我们用gdb来看看
gdb ./main
gdb> break _start
gdb> run
到_start 函数命中断点,开始 stepi 单步的执行汇编指令。
程序从_start开始,调用main,main函数从上到下执行,从地址80480dd执行到80480f0,call返回后,下一条指令又是80480dd,又会调用main,从地址80480dd执行到80480f0 又执行一遍,第二次执行,其实不是本意,
只是刚好call后面的指令恰好是main函数,一直在执行到ret指令,这时,栈顶的值是0x1, 把该值给eip,0x1地址上无有效指令,报段异常,正确的段应该在80480f0附近
这里主要就是因为 call返回后,下一条指令又是80480dd,恰好是main函数,gcc编译的时候,如果使用 -Os 选项,得到的反汇编如下,这时就不会执行2次,但是依然会报段错误,因为指令指针已经飞到其他地方去了。
080480d8 <main>:
80480d8: push %ebp
80480d9: b8 mov $0x8,%eax
80480de: e5 mov %esp,%ebp
80480e0: 5d pop %ebp
80480e1: c3 ret
080480e2 <_start>:
80480e2: e8 f1 ff ff ff call 80480d8 <main>
正确的做法就是:在call main之后,退出进程。
.globl _start
_start: call main
mov $, %eax
mov $, %ebx
int $0x80
调用系统调用_exit(0);正确退出。
反汇编如下:
080480d8 <_start>:
80480d8: e8 0c call 80480e9 <main>
80480dd: b8 mov $0x1,%eax
80480e2: bb mov $0x0,%ebx
80480e7: cd int $0x80 #END 080480e9 <main>:
80480e9: push %ebp
80480ea: e5 mov %esp,%ebp
80480ec: ec sub $0x10,%esp
80480ef: c7 fc fd movl $0x80480fd,-0x4(%ebp)
80480f6: b8 mov $0x8,%eax
80480fb: c9 leave0480fc: c3 ret
如果用C语言来代替之前那个_start的汇编,则如下:
stubstart.c
void _start() {
// main body of program: call main(), etc
// exit system call
asm("mov $1,%eax;"
"xor %ebx,%ebx;"
"int $0x80"
// xor 异或自己就是0,与 movl $0, %eax 具有类似效果
);
}
至此,一个完全不依赖第三方库的简单C函数就完成了。
参考:
https://www.cnblogs.com/TOLLA/p/9646035.html
https://blogs.oracle.com/linux/hello-from-a-libc-free-world-part-1-v2
https://blogs.oracle.com/linux/hello-from-a-libc-free-world-part-2-v2
Linux下一个最简单的不依赖第三库的的C程序(2)的更多相关文章
- Linux下一个最简单的不依赖第三库的的C程序(1)
如下代码是一段汇编代码,虽然标题中使用了C语言这个词语,但下面确实是一段汇编代码,弄清楚了这个代码,后续的知识点才会展开. simple_asm.s: #PURPOSE: Simple program ...
- Linux下一个php+mysql+nginx构建编译(三)
在此之前一直是一个关键构建webserver.但一个关键的建筑环境都比较旧的.假定使用一个相对较新的环境,尤其是正式的server.您必须手动编译自己建(基于以下的结构linux centos6.5 ...
- linux下一个Oracle11g RAC建立(四)
linux下一个Oracle11g RAC建立(四) 三.配置共享存储 配置ASM管理准备 1)OCRDISK :存储CRS资源配置信息 2)VOTEDISK:仲裁盘.记录节点状态 3)DataDis ...
- Linux下一个简单的日志系统的设计及其C代码实现
1.概述 在大型软件系统中,为了监测软件运行状况及排查软件故障,一般都会要求软件程序在运行的过程中产生日志文件.在日志文件中存放程序流程中的一些重要信息, 包括:变量名称及其值.消息结构定义.函数返回 ...
- linux下git的简单运用
linux下git的简单运用 windows下也有git,是git公司出的bash,基本上模拟了linux下命令行.许多常用的命令和linux下操作一样.也就是说,windows下的git命令操作和l ...
- Linux 下一个很棒的命令行工具
导读 Taskwarrior 是 Ubuntu/Linux 下一个简单而直接的基于命令行的 TODO 工具.这个开源软件是我曾用过的最简单的基于命令行的工具之一.Taskwarrior 可以帮助你更好 ...
- linux下一个oracle11G DG建立(一个):准备环境
linux下一个oracle11G DG建立(一个):准备环境 周围环境 名称 主库 备库 主机名 bjsrv shsrv 软件版本号 RedHat Enterprise5.5.Oracle 11g ...
- Linux下MySQL的简单操作
Linux下MySQL的简单操作 更改mysql数据库root的密码 首次进入数据库是不用密码的: [root@localhost ~]# /usr/local/mysql/bin/mysql -ur ...
- linux下一个有意思的问题(文件名以短划线或空格开头)
linux下一个有意思的问题(文件名以短划线开头) 这本是无意中的一个发现. 在linux下,文件名中含有 - 是没有问题,但是如果文件名是以-作为第一个字符的,那么就比较麻烦了. 问题演示 看这里, ...
随机推荐
- SVN教程 -- 基于自己学习记录
SVN教程 -- 基于自己学习记录 1. 概述 a. 什么是SVN? Apache Subversion 通常被缩写成 SVN,是一个开放源代码的版本控制系统.相较于 git ,svn 是集中式版本控 ...
- python 将列表嵌套字典的unicode字符串转换为str格式的字符串的方法
今天在进行django开发的过程中遇到了一个非常棘手的问题, 因为需求原因, 需要将一份数据存为json格式到数据库中, 如下面这种格式: list_1 = [{"name":&q ...
- AssertionError: View function mapping is overwriting an existing endpoint function: admin.main
刚才给views.py文件添加了一个路由地址: @admin_view.route('/test', methods=["get", "post"]) @log ...
- jmeter 之 BeanShell PostProcessor跨线程全局变量使用
BeanShell PostProcessor是用户对一些变量的操作,操作方法很灵活,大概原理是通过parameters传回来对象,然后在script中对对象进行操作 场景:从登陆接口中获取token ...
- Swift 函数提前返回
简评:函数提前返回主要的好处是:将每个错误处理进行分离,审查代码时不需要考虑多种复杂异常,我们可以吧注意力集中在也业务逻辑中,调试代码时可以直接在异常中打断点. 提前返回 首先来看一下需要改进的代码示 ...
- Set的总结
Set最重要的操作是查找,为查找而设计.存入HashSet的元素必须定义hashCode(); Set不保存重复的元素,元素必须唯一.通过equals()方法一确保对象的唯一性. Set中最常被用于归 ...
- Oracle中ROWNUM的使用技巧
ROWNUM是一种伪列,它会根据返回记录生成一个序列化的数字.利用ROWNUM,我们可以生产一些原先难以实现的结果输出,但因为它是伪列的这个特殊性,我们在使用时也需要注意一些事项,不要掉入“陷阱”.下 ...
- PS2模拟器 PCSX2 新手向
1.模拟器的下载 1.1百度网盘地址:http://pan.baidu.com/s/1i3kt7bJ (已经整合了PS2BIOS的模拟器下载,比较新的版本,适合新手) 1.2高端玩家可以下载: 官网g ...
- RocketMQ消息发送的队列选择与容错策略
一个topic有多个队列,分散在不同的broker.producer在发送消息的时候,需要选择一个队列 producer发送消息全局时序图: 队列选择与容错策略结论: 在不开启容错的情况下,轮询队列进 ...
- Android字符串及字符串资源的格式化
为什么要写这一篇随笔呢?最近做项目的过程中,遇到很多页面在要显示文本时,有一部分是固定的文本,有一部分是动态获取的,并且格式各式各样.一开始采取比较笨的办法,把他拆分成一个个文本控件,然后对不同的控件 ...