OS40.【Linux】动态库和静态库 自制动态库

1.补: 默认传参方式
准备
之前在OS39.【Linux】动态库和静态库 自制静态库文章给到了制作静态库的源文件:
 static_lib.c:
写一个简单的函数,方便演示
double mydiv(double x,double y)
{
    return x/y*1.0;
}
static_lib.h:
#pragma once
double mydiv(double x,double y)
但是没有添加特殊情况的处理:如果除数是0,会引发错误,因此需要设置myerrno
改进:
static_lib.h:
#pragma once
double mydiv(double x,double y);
 static_lib.c:
#include "static_lib.h"
int myerrno = 0;
double mydiv(double x,double y)
{
    if (y == 0)
    {
        myerrno = 1;
        return -1;
    }
    return x/y*1.0;
}
那么main.c在调用时应该声明myerrno为外部变量:
#include <stdio.h>
#include "lib/include/static_lib.h"
int main()
{
    extern int myerrno;
    printf("1 / 0 =  %lf myerrno = %d\n",mydiv(1,0),myerrno);
}
使用以下命令:
gcc main.c -L ./lib/libdiv/ -ldiv
但发生了一个很奇怪的问题: 除数为0,但myerrno为0
分析
这和Linux上C语言的函数的默认传参方式有关:
使用在OS12.【Linux】gcc和g++以及动静态链接文章生成Intel格式的汇编代码文件命令:
gcc -S main.c -L ./lib/libdiv/ -ldiv \
-masm=intel \
-fno-asynchronous-unwind-tables \
-fno-dwarf2-cfi-asm \
-fno-exceptions \
-fno-pic \
-fno-pie
main.s:
.file"main.c"
.intel_syntax noprefix
.text
.section.rodata
.LC2:
.string"1 / 0 =  %lf myerrno = %d\n"
.text
.globlmain
.typemain, @function
main:
endbr64
pushrbp
movrbp, rsp
pushrbx
subrsp, 8
movebx, DWORD PTR myerrno[rip]
movrax, QWORD PTR .LC0[rip]
pxorxmm1, xmm1
movqxmm0, rax
callmydiv
movqrax, xmm0
movesi, ebx
movqxmm0, rax
movedi, OFFSET FLAT:.LC2
moveax, 1
callprintf
moveax, 0
movrbx, QWORD PTR [rbp-8]
leave
ret
.sizemain, .-main
.section.rodata
.align 8
.LC0:
.long0
.long1072693248
.ident"GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
.section.note.GNU-stack,"",@progbits
.section.note.gnu.property,"a"
.align 8
.long1f - 0f
.long4f - 1f
.long5
0:
.string"GNU"
1:
.align 8
.long0xc0000002
.long3f - 2f
2:
.long0x3
3:
.align 8
4:
重点关注printf的传参方式:
从x86汇编角度证明是从右向左传参
注: pxor是逻辑异或,根据Intel® 64 and IA-32 Architectures Software Developer’s Manual的描述,PXOR xmm1, xmm2/m128类别,是Bitwise XOR of xmm2/m128 and xmm1,图中的pxor xmm1,xmm1是将xmm1和xmm1自身进行XOR运算,将结果存储到xmm1中,即将xmm1寄存器清零
因为是从右向左传参,那么mydiv还没有执行时,myerrno的值就传入了printf的参数中,这会导致错误的结果
解决方法1: 修改汇编代码,修改传参方式
由于myerrno的值可以通过DWORD PTR myerrno[rip]访问到,要传入mydiv修改后的myerrno的值,那么可以这样做:
将原来在sub rsp,8下方的mov ebx, DWORD PTR myerrno[rip]放到movq rax,xmm0的下方
再生成可执行文件:
gcc -c main.s -o main.o
gcc main.o -L ./lib/libdiv/ -ldiv -no-pie
注: -no-pie下文会讲到
运行结果:
解决方法2: 遵循传参规则,修改main.c源代码
#include <stdio.h>
#include "lib/include/static_lib.h"
int main()
{
    extern int myerrno;
    double ret = mydiv(1,0);
    printf("1 / 0 =  %lf myerrno = %d\n",ret,myerrno);
}
运行结果:
errno的本质
从上方的代码可以看出: errno的本质是一个全局错误码,更严谨来说,errno是系统调用接口的全局错误码
补充: pascal调用约定
可能有部分读者想到从左向右传参,这样传入printf中的myerrno就是mydiv函数修改后的值了,即传参次序为
1."1 / 0 =  %lf myerrno = %d\n"的地址
2.ret的值
3.myerrno的值
先说结论: 不可以,现代的gcc/g++都不支持从左向右传参方式,又称pascal调用约定
看看古老的pascal语言编译器的传参方式
Windows7下安装Free Pascal IDE,之后按下图操作:
改成437美国,防止乱码:
主界面:
File->New File创建新的源文件,写入以下代码:
uses crt;
var
  x,y,ret:integer;
function test(a, b: integer): integer; pascal;
begin
  test:=a+b;
end;
begin
  writeln('Complier : Free Pascal IDE 2.4.0');
  write('input two integer number:');
  readln(x,y);
  ret:=test(x,y);
  writeln('x+y=',ret);
  delay(2000);
end.
使用pascal IDE自带的反汇编器:
光标先定位到此处:
Run->Goto Cursor
输入以下内容:
Debug->Disassemble
看到以下反汇编指令:
使用Goto Cursor功能,运行到第二个movzwl后查看寄存器:
eax值先为0x00000001
eax值后为0x00000002
确实是从左向右传参
运行结果:
附: 下载链接
有关Free Pascal IDE的安装包和其他文件可以到我的网盘http://zhangcoder.ysepan.com/中的/CSDN上的资料/pascal调用约定研究中下载
采用分卷压缩:
2.制作动态库
生成动态库和生成静态库类似,必须先形成o文件才能生成动态库文件
仍然以OS39.【Linux】动态库和静态库 自制静态库文章的mydiv函数为例,将这个函数打包为动态库
dynamic_lib.h:
#pragma once
double mydiv(double x,double y);
dynamic_lib.c:
int myerrno = 0;
double mydiv(double x,double y)
{
    if (y == 0)
    {
        myerrno = 1;
        return -1;
    }
    return x/y*1.0;
}
重写Makefile:
lib=libmydiv.so
$(lib):dynamic_lib.o
gcc -shared -o $@ $^
dynamic_lib.o:dynamic_lib.c
gcc -fPIC -c $^
.PHONY:clean
clean:
rm -f dynamic_lib.o libmydiv.so
.PHONY:relase
release:
    mv libmydiv.so ./lib/libdiv/libmydiv.so
注: fPIC是产生位置无关码(Position Independent Code),具体细节后面再说
依次执行:
make
make release
最终生成libmydiv.so文件
为什么动态库需要有可执行权限?
查看libmydiv.so的权限:
发现动态库需要有可执行权限,静态库存在于可执行文件内部,但动态库在可执行文件的外部,这就导致如果进程要执行动态库的函数,必须跳转到动态库中执行,即动态库必须被加载,执行里面的库函数,换句话说,动态库是共享库
这个"加载"的含义是: 如果某个文件有可执行权限,那么这个文件会以可执行程序的形式加载到内存中
进而得出:1.动态库不能单独执行,必须由进程调用,因为动态库中没有main函数
               2.动态库被系统加载后,会被所有进程共享
动态库的加载方法
运行着的操作系统上有大量活跃的进程,这些进程或多或少都需要调用动态库,那么系统需要加载大量的动态库
加载动态库的本质是将动态库文件数据写入物理内存中,在开启分页功能下,需要将库映射到进程地址空间的共享区中
这个共享区如图:
之后后,执行的任何代码都是在进程地址空间中执行
调用库函数需要跳转到进程地址空间的共享区中执行,执行完毕后返回调用库函数的下一条指令
注意: 如果有多个进程调用同一个动态库,操作系统是不会重复拷贝同一个库到物理内存中的,而且操作系统是很清楚所有库的加载情况的
,理由: 1.重复加载相同的动态库浪费物理内存 2.动态库又称共享库,被所有进程共享,例如下图:
3.链接动态库
gcc命令和静态库类似,需要指定:
1.头文件路径:
-I ./lib/include/
2.头文件名:
#include "dynamic_lib.h"
2.库的路径:
-L ./lib/libdiv/
3.库名:
 -lmydiv
编译:
gcc main.c -I ./lib/include/ -L ./lib/libdiv/ -lmydiv
但运行报错:
guest@zhangcoder:~$ ./a.out
./a.out: error while loading shared libraries: libmydiv.so: cannot open shared object file: No such file or directory
使用ldd检查:
虽然告诉了头文件路径、头文件名、库的路径、库名,但发现自制的动态库ldd命令找不到地址(not found)
程序形成后和编译器没关系,虽然告诉了gcc,但动态库的位置没有告诉操作系统
结论: 动态库的位置必须告诉操作系统,否则操作系统无法加载程序指定的动态库
4.让操作系统的加载器找到自制的动态库
方法1: 临时添加环境变量
:临时添加环境变量LD_LIBRARY_PATH搜索用户自定义的动态库的路径,下一次登录服务器又会失效
LD_LIBRARY_PATH默认没有:
需要手动添加PATH路径(不要写文件名):
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/guest/lib/libdiv
AI写代码
cpp
运行
在OS21.【Linux】环境变量文章提到过: $LD_LIBRARY_PATH号说明LD_LIBRARY_PATH是系统变量名
运行结果:
用ldd再次查看:
libmydiv.so已经有地址了
加载器能找到自制C库的原因: 环境变量给了动态库的搜索路径
简单理解ldd打印的地址的含义
man手册上是这样说的:
For each dependency, ldd displays the location of the matching object and the (hexadecimal) address at which it is loaded.
是内存中的虚拟地址
方法2: 将环境变量写入启动脚本
LD_LIBRARY_PATH环境变量写入启动脚本,这样每次登录服务器时就会LD_LIBRARY_PATH自动添加到系统的环境变量
介绍主目录下的几个隐藏文件
.bash_history
存储历史输入过的命令,可参考了解: https://www.redhat.com/en/blog/history-command
.bash_logout
logout是登出的意思,那么这个文件指用户使用exit或logout登出系统时需要执行的脚本,博主服务器上是这样的:
# ~/.bash_logout: executed by bash(1) when login shell exits.
# when leaving the console clear the screen to increase privacy
if [ "$SHLVL" = 1 ]; then
    [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q
fi
可参考了解: https://superuser.com/questions/410525/explain-why-bash-logout-wont-run-commands
.bashrc
是shell的初始化脚本
可参考了解: https://www.ibm.com/docs/en/product-master/12.0.0?topic=perl-sample-bashrc-file
和https://www.debian.org/doc/manuals/debian-handbook/sect.shell-environment.en.html
.profile
默认情况下,.bashrc已经引入(sourcing)了.bashrc,而且.profile可以设置能被bash子进程继承的环境变量
博主服务器上是这样的:
# ~/.profile: executed by the command interpreter for login shells.
# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
# exists.
# see /usr/share/doc/bash/examples/startup-files for examples.
# the files are located in the bash-doc package.
# the default umask is set in /etc/profile; for setting the umask
# for ssh logins, install and configure the libpam-umask package.
#umask 022
# if running bash
if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
    fi
fi
# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi
# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
    PATH="$HOME/.local/bin:$PATH"
fi
来源: https://www.debian.org/doc/manuals/debian-handbook/sect.shell-environment.en.html
其他参考网站: https://askubuntu.com/questions/1411833/what-goes-in-profile-and-bashrc
https://www.ibm.com/docs/en/aix/7.1.0?topic=files-profile-file
写入启动脚本.profile
不要改变.profile的原来已经写入的内容,底下添加export命令
运行结果:
方法3: 拷贝动态库或建立软链接到系统搜索动态库的目录中
从上图看: 可以将拷贝动态库或者软链接放到/lib64或者/lib/x86_64-linux-gnu/目录中
方法4: ldconfig 配置/etc/ld.so.conf.d/并且ldconfig更新
ld.so.conf.d以d结尾,显然是个目录
以conf为后缀的文件存储的是路径,比如:
这里照葫芦画瓢,可以创建自己的conf文件(需要root权限):
写存储库文件的路径,不用写库名字
/home/guest/lib/libdiv
再用ldconfig重新加载全局范围内生效的配置文件
sudo ldconfig
运行结果:
实际上,安装第三方库都是直接安装到系统的
5.库中的全局变量问题
C语言库中有一个errno全局变量,假设多个进程一起修改,会出现进程干扰的问题,怎么解决?
答: 不会出问题,共享区在堆和栈之间,写入时发生写时拷贝
————————————————
版权声明:本文为CSDN博主「zhangcoder」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_85828611/article/details/154395981
阅读剩余
THE END
阿里云ECS特惠活动
阿里云ECS服务器 - 限时特惠活动

云服务器爆款直降90%

新客首单¥68起 | 人人可享99元套餐,续费同价 | u2a指定配置低至2.5折1年,立即选购享更多福利!

新客首单¥68起
人人可享99元套餐
弹性计费
7x24小时售后
立即查看活动详情
阿里云ECS服务器特惠活动