开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

个人账号如何用微信支付开发,试试蓝兔支付吧

发表于 2023-12-14

一、背景

支付功能是面向C端系统的一个非常典型的功能,而对于很多刚接触支付功能开发者会出现望而止步的现象,大部分的支付平台都需要用企业账号的身份进行签约,导致很多开发者想学习和体验支付功能,受到了一定的阻碍。

既然上有政策,那么必然会出现下有对策的解决办法,只要有问题,就一定会有人解决。

而有一些基于第三方支付平台的服务商模式的签约企业,便为个人开发者提供了支付功能,为广大的普通用户集成支付提供了环境,我们可以用其提供的环境来实现个人账号开通微信支付、支付宝支付,本文带领大家实现一个蓝兔支付平台的个人商户签约。

如今这个时代,有一个属于自己的支付商户号是非常有必要的,因为微信支付和支付宝支付默认不支持个人用户申请的,没有营业执照,那么很多人学习了视频教程中的商城项目、外卖之类的项目就无法做到订单的第二种状态:已支付,无疑是阻断了开发者的技能和学习成长。很多写了苍穹外卖、商城系统、交易类系统的同学无法真正让项目变的真实,那么通过自己申请签约个人支付平台,可以让自己具备对接支付的经验,可以在一定程度上讲自己当前的项目变活,去遇到更多的问题来迫使自己不得不解决。

二、正文开始

2.1、申请前提

(1)、最好有一个自己当前已经备案过的域名,与可访问的网站,签约时需要填写一个备案的域名

(2)、个人银行卡的财产余额至少50元及其以上,需要拿出50元作为签约开通的费用

2.2、蓝兔支付平台介绍

本文教给大家的是蓝图支付,蓝兔支付是支付宝、微信支付官方合作伙伴,可以支持个人、个体户、企业支付接口申请,官方直接签约,无需营业执照,最快10分钟内签约完成,签约后可使用支付宝商户、微信支付商户相关接口能力,资金由支付宝、微信支付官方直连结算。作者在最近的测试过程中发现,蓝兔支付的支付宝支付由于风控的问题,不提供支付宝支付了。

提交申请资料》平台审核》微信官方审核》微信扫码签约》支付开户费用》商户号开通》完成根据API文档接入

官网网址为:www.ltzf.cn

产品介绍如下:

产品支持的范围如下:

2.3、注册蓝兔支付平台

1、打开蓝兔支付的网页,选择快速注册,弹出微信二维码,我们使用手机进行二维码扫描。

2、手机扫码后,成功关注公众号后,会自动注册账号,并提示注册成功,同时PC端会自动跳转到系统的首页面:

3、接下来我们完善下个人的资料,补充完基本资料后,进入到蓝兔支付的首页面:

接下来按照如下流程进行操作:

提交申请资料》平台审核》微信官方审核》微信扫码签约》支付开户费用》商户号开通》完成根据API文档接入

2.4、提交申请资料

1、选择左侧的微信支付菜单中的申请签约选项,在下方的界面中填写自己真实的资料,尤其是网站地址,最好自己有个已经备案的公网域名地址:

按照真实的情况填写数据,最好有个属于自己的公网的备案的域名。

2、然后再第二步的界面中,上传自己的身份正反面的照片,然后输入自己的手机号和邮箱,单击下一步提交:

3、提交成功后,会提示您的签约资料已提交成功,这里我们等待10分钟即可,如下所示:

到此,剩下的流程如下:

平台审核》微信官方审核》微信扫码签约》支付开户费用》商户号开通》完成根据API文档接入

2.5、平台审核

提交完资料后,查看左侧的签约记录菜单,可以看到当前签约记录正在审核:

2.6、微信官方审核

大概10分钟过后,状态会变成官方审核中:

2.7、微信扫码签约

大概几分钟后,官方审核通过,此时状态改变,会提示待商户签约,这里选择签约按钮,弹出二维码进行签约处理,扫码后会提示确认签约商户信息即可。

2.8、支付开户费用

签约确认成功后,状态改变,变成为待支付费用,此时单击支付按钮,显示支付50元的二维码,此时支付即可:

2.9、商户号开通

支付完成,会提示成功,如下所示:

此时选择左侧的商户管理菜单,可以看到自己的商户信息,单击详情可以看到具体信息,如下所示:

2.10、API测试访问

终于到了最后一步,我们此时可以新创建工程,生成PC端的支付二维码完成支付操作即可。

1、首先在官网上选择API文档或者下载中心,下载Demo体验,我这里选择的API文档,没有用官方提供的Demo样例,因为我有一些过往的支付对接的经验,建议大家可以选择下载中的Demo,下载中心的Demo是一个支付的样例工程,用JSP开发了基本的流程,可以研究运行下,改改参数就可以运行。

可以根据下文中的几个测试类,来体验蓝兔支付,本文并没有讲解支付回调。相对于原生的第三方平台的微信支付和支付宝支付的开发难度,蓝兔支付平台的代码开发相对来说较为简单。

地址在这里:蓝兔支付文档

2、本文不会开发出完整的支付流程,只验证当前是否可以成功创建支付请求,是否可以成功返回下单的URL即可。

创建一个maven项目,并导入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.28</version>
</dependency>
</dependencies>

3、创建加密签名类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
typescript复制代码public class PaySignUtil {

/**
* 支付参数签名
*
* @param params 需要参与签名的参数
* @param partnerKey 商户密钥
* @return {String} 参数签名
*/
public static String createSign(Map<String, Object> params, String partnerKey) {
// 生成签名前先去除sign
params.remove("sign");
String stringA = packageSign(params, false);
String stringSignTemp = stringA + "&key=" + partnerKey;
return SecureUtil.md5(stringSignTemp).toUpperCase();
}

/**
* 组装签名的字段
*
* @param params 参数
* @param urlEncoder 是否urlEncoder
* @return {String}
*/
public static String packageSign(Map<String, Object> params, boolean urlEncoder) {
// 先将参数以其参数名的字典序升序进行排序
TreeMap<String, Object> sortedParams = new TreeMap<String, Object>(params);
// 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Entry<String, Object> param : sortedParams.entrySet()) {
String value = String.valueOf(param.getValue());
if (StrUtil.isBlank(value)) {
continue;
}
if (first) {
first = false;
} else {
sb.append("&");
}
sb.append(param.getKey()).append("=");
if (urlEncoder) {
try {
value = urlEncode(value);
} catch (UnsupportedEncodingException e) {
}
}
sb.append(value);
}
return sb.toString();
}

public static String urlEncode(String src) throws UnsupportedEncodingException {
return URLEncoder.encode(src, "utf-8");
}

}

4、定义测试生成PC端二维码的支付类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
ini复制代码public class LantuPayUtil {
public static final String mchId = "自己的商户号";
public static final String notifyUrl = "支付回调地址";
public static final String key = "自己的商户秘钥";
public static void main(String[] args){
//商户订单号,只能是数字、大小写字母_-且在同一个商户号下唯一。
String out_trade_no = "2023" + System.currentTimeMillis();
String total_fee = "0.01"; //支付金额
//商品描述
String body = "商品描述参数";
Long stime = System.currentTimeMillis()/1000;
//当前时间戳
String timestamp = String.valueOf(stime);
Map<String, Object> map = new HashMap<>();
map.put("mch_id", mchId);
map.put("out_trade_no", out_trade_no);
map.put("total_fee", total_fee);
map.put("body", body);
map.put("notify_url", notifyUrl);
map.put("timestamp", timestamp);
String sign = PaySignUtil.createSign(map, key);
map.put("sign", sign);
//微信扫码支付接口地址
String url = "https://api.ltzf.cn/api/wxpay/native";
String result = HttpRequest.post(url).form(map).execute().body();
System.out.println(result);
String msg = "";
if (StrUtil.isBlank(result)) {
msg = "API接口返回为空,请联系客服";
}else{
try{
JSONObject jsonObject = (JSONObject) JSONObject.parse(result);
Integer code = jsonObject.getInteger("code");
if(code==0){
String qrcode = jsonObject.getJSONObject("data").getString("QRcode_url");
msg = "请使用微信扫码下方二维码:" + qrcode;
}else{
String msgstr = jsonObject.getString("msg");
msg = "返回失败:" + msgstr;
}
} catch (JSONException e) {
msg = e.getMessage();
} catch (Exception e) {
msg = e.getMessage();
}
}
System.out.println(msg);
}
}

将程序中的3个常量换成自己商户信息即可,接口调用成功后,控制台会返回二维码的路径:

我们在浏览器中,打开这个URL,进行扫码支付,支付成功后,可以在蓝兔的后台看到支付的账单结果:

至此,我们作为个人签约蓝兔支付平台,可以成功的对接了支付API,以后我们就可以开发出支付功能的系统了,瞬间变的高大上了。

三、总结

今天分享了,如何申请个人签约类型的支付平台商户信息,如果自己想要学习支付功能,不如马上去动手实践吧,蓝兔平台的签约过程时间非常快,工作时间不到30分钟就审核完成了。我同时发现蓝兔官方并没有单独封装Java SDK,只是提供了HTTP请求的样例代码,而这种没有的东西,正式我个人最近计划要做的内容和目标,我计划最近做一个开源的蓝兔支付的Java SDK,方便大家使用,毕竟目前很多人都因为没有支付信息,让自己停留在了学习阶段。除了蓝兔支付,市场上还有个支持个人签约的YunGouOS,他们家的支付SDK建设的比蓝兔支付的全且完整一点。

同时本文也是作者的个人知识星球【觉醒的新世界程序员】中最近正在分享的开源设计系列专题中的其中一篇文章,如果想了解相关内容,可以来了解下。目前计划根据某个开源的SDK工具包为例子,来学习它的设计思路和代码封装结构,希望可以帮助大家学会和掌握SDK的设计和最佳实践等,目前计划分享的这个SDK是微信支付工具包wxjava,这个项目对于微信下的API对接封装的非常不错,非常值得大家学习,比如微信公众号、微信小程序、微信支付等等,都可以十分方便集成到项目中,在2021年的时候,由于工作关系,发现其里面有个逻辑不支持正向代理,于是在拉取源码后,增加了适配逻辑,并进行了PR,成功在公司项目中用到了自己贡献的代码。

近期的分享将会在最后仿照他设计一个支付SDK的工具包,作为目标产出。对于日常业务功能的开发,多少会遇到HTTP的三方接口调用的例子,那么学习这个可以让自己设计出更佳优雅的SDK,以便加强自己。

如果本文对你有用,可以去实践哦,蓝兔网站的直达链接单击这里。

如果本篇文章对你有用,收藏、关注、分享、点赞哦。

如果你想一起参与蓝兔支付Java SDK开源项目的建设,可以关注我的Github吧,最近将会写一个针对蓝兔支付的SDK。目前Github的仓库地址如下:

github.com/wuchubuzai2…

感兴趣的可以和我一起来设计封装下蓝兔支付的SDK。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 上了 Apple 第三方重大列表,2024

发表于 2023-12-13

这个话题的起因来自 2023 年 WWDC 之后苹果发布的「App Store 提交隐私更新」政策,政策主要提出了两点:第三方 SDK 隐私清单和签名和需要提供必要理由的 API 流程。

其实先简单总结,就是 Apple 想通过隐私清单来进一步提升用户数据收集和使用的透明度,包括 required reason API 也是算是属于隐私清单里的内容。

那这个和标题里的 Flutter 有什么关系?因为跟随此次隐私清单升级, Apple 上线了一个 对用户隐私产生重大影响的第三方 SDK 列表 ,而在这列表恰好就有 Flutter 的存在,并且里面接近 1/3 是和 Flutter 相关的 Plugin ,例如 connectivity_pulus 、device_info_plus 、fluttertoast 、shared_preferences_ios 、sqflite 、webview_flutter_wkwebview 、url_launcher 等等。

列表地址:developer.apple.com/support/thi…

这就很懵逼了,是 Flutter 做了什么「天怒人怨」的问题,还是 Apple 故意针对 Flutter 呢?那在聊这个问题之前,我们首先需要简单了解下,这次的隐私标签里主要涉及的是什么?

这个问题不只是对 Flutter ,其实对于 iOS 来说 2024 在 Xcode15 上也属于最重要的适配需求。

隐私清单

聊隐私清单,首先最主要是就是第三方隐私清单(privacy manifest),属于让 SDK 开发人员提供他们 SDK 的一些数据收集行为,然后 App 开发者在集成各种 SDK 之后,可以得到一份详细的隐私清单,如下图是 SDK 里声明的隐私清单。

隐私清单的作用是帮助开发者了解第三方 SDK 如何使用数据,这样开发者在向 App Store 提交审核时,Xcode 可以将第三方 SDK 中的隐私清单合并,然后导出一个 PDF 报告汇总。

通过这份隐私清单报告,开发者就可以在 App Store 提交审核时,更方便地提供「隐私标签」,如下图所示就是 iOS 14 时 App Store 推出的功能,当时就要求 App 开发者在 App Store 后台声明应用的数据收集和使用场景,让用户在应用详情页面知道 App 收集了什么和做了什么。

那么首先可以知道,SDK 隐私清单的作用,就是让 App 开发者可以根据生成的清单,更好精确和方便地管理自己 App 的隐私标签,一般情况下大概会有:

  • 收集的数据类型
  • 数据是否能关联到用户
  • 数据是否用于跟踪活动
  • 收集此数据的理由

其中关于数据类型可见:developer.apple.com/documentati…

那如果我不需要隐私清单来管理隐私标签,就可以理会这次的隐私清单的事情了呢?那答案肯定不是,因为隐私标签还包含了其他东西,比如必要理由的 API 声明、声明跟踪域名和第三方 SDK 签名等。

必要理由的 API 声明

首先这个主要是 Apple 新搞了一个 API 分类, 使用这一个 API 分类你需要在隐私清单里说明使用理由, Apple 这么做的目的是主要是想规范 App 随意通过这些 API 做 fingerprinting 识别的行为,相关的 API 也不多,大概涉及:

  • File timestamp APIs
  • System boot time APIs
  • Disk space APIs
  • Active keyboard APIs
  • User defaults APIs

如果 SDK 和 App 里用到了分类里的这些 API ,你就需要在隐私列表里填写原因,例如:

使用了 Disk space APIs 的 systemFreeSize ,在写入前判断磁盘空间大小,那么就需要引用列表里的 E174.1 条款,如下图写明使用的原因。

从目前看,类似 UserDefaults、stat 等 API 还是很容易被使用到,所以 Required Reason API 这部分应该都是跑不掉的。

条款列表可见:developer.apple.com/documentati…

声明跟踪域名

隐私清单其实一定程度也算是应用跟踪透明度 (ATT) 的内容之一,而本次更新,如果用户未通过 App Tracking Transparency (ATT) 授予权限, Apple 就会阻止对追踪域(Tracking Domains)的网络请求。

简单来说,就是需要定义一个 NSPrivacyTrackingDomains 列表,这样但用户不同意 ATT 权限时, 系统就会阻止这些域名的任何调用,所以 SDK 上如果有类似相关跟踪的域名调用,也需要区分声明好跟踪域名和非跟踪域名。

当然,不管你是否利用 ATT 的定义进行跟踪,你都必须在隐私清单中列出收集的数据类型和原因,因为这个和 ATT 没有必然关系,只是如果使用了 ATT 的域名,在这里需要额外声明来符合权限, ATT 在 iOS 17 上实际并没有什么变化。

这里的跟踪定义是:能狗将你收集的数据与其他公司 App 收集的数据相关联,比如使用了 IDFV 也算跟踪,。

只是这里我有个疑问,如果我不把 ATT 的相关收集域名放到 NSPrivacyTrackingDomains 列表,其实 Apple 是审核是不是也无法明确发现呢?那么我不做是不是也是可以?

第三方 SDK 签名

本次跟随隐私清单而来的还有第三方 SDK 签名,这是我们以前一直忽略的东西,而 Apple 的目的也很直接,就是希望通过签名认证的方式来确保 SDK 不会被篡改,简单的说就是 SDK 发布时带有 _CodeSignature 。

需要签名,肯定是 SDK 包含了二进制依赖,没有二进制依赖其实并不需要考虑签名问题。

这里的签名其实分 Apple Developer Program 签名和自签名,如果你的 SDK 使用的是 Apple Developer Program ,简单来说就是可以得到更安全可靠的认知加持,安全性拉满,但是如果你采用的是 codesign 自签名,那么也不是不行,就是你自己保管好签名认证的方式。

1
2
3
4
5
6
7
8
shell复制代码echo "Build Archive Device Slice"
xcodebuild clean archive -sdk iphoneos -destination 'generic/platform=iOS'[...]
echo "Build Archive Simulator Slice"
xcodebuild clean archive -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' [...]
echo "Create XCFramework"
xcodebuild -create-xcframework [...] -output <YOUR.xcframework>
echo "Codesign XCFramework"
codesign --timestamp -v --sign "<YOUR CERTIFICATE (ABCXYZ)>" "<YOUR.xcframework>"

经过签名认证后的 SDK ,在 Xcode15 会显示对应的 Signature 信息,如果一旦发现本次前面和上次不一致,那么Xcode 就会让编译失败并弹出警告。

当然,如果 SDK 的签名变更符合你预期,比如 SDK 维护人员发生了变化,从而导致证书发生变化,那么你也可以通过接受变更的方式也同意更改。总的来说目前这个阶段签名并不是一定要官方,甚至不一定就强制要签名,Apple 「鼓励】所有 SDK 使用签名,但影响隐私的 SDK 一定要签名,特别是前面提到列表里的。

目前来说,App Store 已经开始检查最近提交的 App 是否包含影响隐私的 SDK ,如果影响隐私的 SDK 不包含签名和隐私清单,Apple 将向应用开发人员发送提醒邮件,包括提供必要理由的 API 部分,最后会在 2024 年春季开始要求审核。

更多官方资料:

  • developer.apple.com/documentati…
  • developer.apple.com/documentati…
  • developer.apple.com/documentati…

Flutter

那本次隐私清单对于 Flutter 来说,最大的影响就是大量 Flutter Plugin 被挂上了 Apple 官方的用户隐私产生重大影响的第三方 SDK 列表,因为上了列表,就被认定为是影响隐私的 SDK,那么隐私清单是跑不了的了,另外如果有二进制依赖还需要满足签名条件,所以这对于 Flutter 来说在 iOS 17的适配上无疑「雪上加霜」,可以看到目前官方开了很多关于官方 package 的 iOS 隐私清单适配 issue 。

对于 Flutter Engine 部分,其实官方适配隐私清单难度并不大,目前调查完 iOS 上 Flutter Engine 主要需要适配的有:

  • File timestamps :
+ `C617.1`  用于 `FlutterAppDelegate.mm` 里的应用状态恢复
+ `0A2A.1` 用于实施相关 `File` 包装器
  • System boot time:
+ `35F9.1` 用于各种事件计时和经过时间的计算

目前官方在 #131494 已经表示,会将 PrivacyInfo.xcprivacy 包含在未来 Flutter.framework 中,所以这部分并不需要担心。

而在 flutter/packages 部分目前却是有棘手的问题,例如 shared_preferences ,它被列入影响隐私 SDK 列表的原因是因为 NSUserDefaults ,但是 shared_preferences 它本身只是 API 的封装,为了方便 Flutter 开发者调用,它本身是不知道如何/为什么使用它,在 SDK 层面很难在隐私清单描述给出所谓的「收集原因」。

另外第三方 Plugin 也可能使用 shared_preferences ,但是 App 开发并不知道它用来做什么,那么如果让 pub 的 Plugin 能描述好隐私清单的内容?

目前官方文档提及:

If you use the API in your third-party SDK’s code, then you need to report the API in your third-party SDK’s privacy manifest file

这听起来像是只要拥有它的代码就需要标记出它的原因,但是实际上 Plugin 不知道该层存储的数据是什么或如何被使用。

对于 “收集” 的定义目前很模糊,类似的还有 webview_flutter ,webview_flutter 本身不收集任何内容,但是App 可以用来 webview_flutter 收集浏览历史记录,然后这如何在 SDK 的隐私清单里去体现?

感觉目前的 iOS 要求没有很明确,适配方向不够清晰。

最后,目前文档说他们「鼓励」每个人都采用该清单,而对于在隐私列表的 SDK 看起来是强制的,但是如何选择这些 SDK 的规则目前也看不到,所以只能等待后续和 Apple 的沟通回复。

更多进度可见:

  • github.com/flutter/flu…
  • github.com/flutter/flu…
  • github.com/flutter/flu…

最后

总结一下,本次 iOS 隐私清单主要覆盖的有:

  • SDK 提供隐私清单的数据收集类型、使用描述和用途
  • 必要理由的 API 需要提供使用“代码”和原因
  • ATT 跟踪添加域名区分
  • 第三方 SDK 签名

这里面 SDK 提供的隐私清单,我个人理解:

  • 首要是用来给 App 打包后,通过导出的 pdf 参考管理隐私标签
  • 其次对于必要理由的 API 的使用附带使用说明
  • 关于 ATT 收集相关的数据域名添加到清单进行区分,至于你不写是否会被抓住不好说
  • 第三方 SDK 签名不是强制,也可以自签名,前提是你不是在官方影响隐私 SDK 列表中
  • 如果你 SDK 没有二进制,没有使用必要理由的 API,理论上其实甚至可以不管什么隐私清单和签名,因为你正常也进不去官方的影响隐私 SDK 列表。

本次更多是探讨,因为官方的描述和文档内容上其实并不严谨,甚至有很多模糊的地方。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节三年,谈谈一线团队如何搞工程化一(全景篇) 从一个故事说

发表于 2023-12-12

前言

小编所在的团队叫 “Cuckoo FE” (Cuckoo:布谷鸟,多数种类为灰褐或褐色,喙强壮而稍向下弯,一只幼鸟一天能吃39只蚱蜢),寓意朴实能干,下文中工程化实践各类体系也是以Cuckoo开头。

目前前端团队成员接近20人,归属字节商业化业务中台,承担海外商业化交易流程建设,为字节内部出海产品提供商业变现能力,整体技术方向以PC中台为主(90%+),另外包含部分移动端项目。

从一个故事说起

小马加入公司不久,最近刚接手一个新需求。凭借以往经验,这是一个small case,很快的给了排期,准备开干。没多久,他就向他的mentor提了第一个问题:“怎么创建项目?”,mentor很快就丢过来一个仓库,并回复道“你拿着这个项目去copy下改改”。小马愣了一下,不敢多问,立马开始操作,一顿CV猛如虎,项目终于跑起来,心想“快了,只要项目跑起来就快了”。接着小马在开发过程中遇到一个比较通用的功能,又问mentor有没有类似组件,没多久mentor又丢过来一个仓库,并在链接下评论:“你在xx文件下找找xx组件,应该修改修改就能用”,小马又把项目跑起来了,细研究了源码,果然能用,很开心又解决了一个问题。这样类似问题一个接一个,mentor也都能很快的给出答复。时间很快,在mentor的加持下到了联调那一天,小马心里长舒一口气“终于可以用真实数据跑一把了”,结果在浏览器刷新的那一刻,页面白了,虽然屏幕有点脏,但显的格外的白,这终究还是没有超出预期啊。这一调又是一堆问题,要么是成功码不对,要么是该有的字段没有,有了结构也不对,小马跑过去问后端怎么回事,后端回道“你应该没看最新文档”,小马打开文档对比了下:“嗯,是已经有很多不一样的了,但和现在的接口返回好像也不完全一样吧”,最终小马还是选择了相信接口返回的json。又是一顿折腾准备提测,小马又问mentor“怎么部署啊?”,mentor毫无意外的又丢过来一个文档,好一点的是这次只有一个文档,应该比较简单,点开的一瞬间又绷不住了,里面是一堆文档的链接,有什么编译的、部署的、环境配置的、权限配置的、流水线校验等等。此时小马已经濒临崩溃,但也只能硬着头皮挨个看,挨个对接,但总有地方报错。去查文档,但文档点开总有其它文档,点不完,有时还会有好几篇相同的文档,但说法却不尽相同(小马心里苦啊,他要的只是一个button,轻轻一点就能部署罢了)。作为新同学,小马只能一遍又一遍的问自己的mentor。

终于经过小马几天持续奋战下,mentor也很给力,项目完成了提测,虽然晚了几天,mentor拍了拍肩膀鼓励道“没事,你还不熟悉,后面就会变好”,小马心里也很庆幸“能遇到这么好的mentor,不然一行代码也写不了”,但回头望着工作桌面满屏的文件夹,又总感觉哪里不对。

小编同学加入字节3年半,所在的团队从开始的几个人到现在的接近20个人,团队成长过程中也遇了一些和小马类似的问题:研发资产沉淀不足、规范不统一、文档缺失、不正确,dev ops 流程不顺畅等等,我们答案是“做好团队工程化体系建设”。但作为业务团队,一切以业务为优先,没有足够的人力投入的工程化等基建上怎么办?所以我们建设的重心有两点:

一,业务团队要做好与公司基建、社区沉淀的连接,尽量利用已有的解决方案快速融合到自己的团队,不做底层的重复建设。

二、做好团队上层沉淀,围绕团队内部个性化诉求打造高效解决方案。

接下来就一起看看在工程化上我们到底做了哪些事情吧,同时也打个小广告,我们准备把这些技术建设写成系列文章(系列主题见下图)分享给大家,欢迎和大家一起交流。

什么是工程化

前端工程化定义

前端工程化作为软件细分领域中的一种技术理念目前并没有明确的定义,它是在15年webpack诞生后开始被越来越多人提及,从开始的打包工具到现在的各类开箱即用解决方案,能力边界在不断扩展。目前社区内基本的共识是以降低成本、提高效率和质量为目标,以流程、工具、规范为手段的实践体系。它是基于实践的一系列解决方案,而非单独某种问题的答案,用一句话概括即前端领域内一站式解决方案。

工程化的发展历程

工程化源于实践,每个时期要解决的问题不同,按照解决问题层级大概可以分为以下几个阶段:

1.0 页面动态化, 这个阶段标志前端领域兴起,从最开始的简单脚本到以JQuery为代表的各类工具库百花齐放。

2.0 模块化阶段, 这个阶段主要解决大型应用的开发难度以及开发效率问题 。 产品代表有国内一线互联网的一系列大型应用如淘宝网,技术则有webpack为代表的构建工具以及React 、Vue、Angular为代表应用各类应用框架。

3.0跨平台大前端阶段,这个阶段重新定义了前端,前端领域也有了这样一个伟大的宏愿:一切能用前端实现的终将都会用前端实现。比较有代表性的产品有小程序、脸书,在线excel等一系列跨端产品,技术代表有ReactNative、Flutter、Electron等各类跨平台开发框架。

4.0alpha智能化阶段,随着AIGC的兴起,辅助开发,辅助应用生成已经初显趋势,同时AI进化的速度是指数级别的,AI+的智能前端时代未来充满想象力。

现在我们已经知道了工程化是什么,接下来来看下具体的实践吧。

工程化实践

作为一个横向支撑团队我们面对的主要挑战有:

快速响应:业务发展较快,不同方向变现诉求强烈,需要具备能同时支持多条业务线能力。

业务复杂高: 面对业务形态多样,国家、用户群体各不相同,个性化差异大,整体业务复杂度较高。

质量线就是生命线: 主要和钱打交道,任何一个不起眼的问题都有可能给用户及公司带来难以估量的损失。

面对这些问题已经不是单纯不断加人就能解决的(ps:人也不好加,不是你想加就能加…),为了解决这些问题,我们团队从规范统一、研发资产沉淀、系统解决方案、创新应用逐步建设自己的工程化体系(详见下图),通过这些技术体系建设来不断提高研发团队的上限,这其中最最重要(小马表示真的很重要)的一件事就是“团队规范建设”。

规范建设

为什么要做规范统一

团队刚成立时,人不多,业务发展速度非常快,团队重心是“敏捷”,各个小团队各自为战,怎么快怎么来,缺少最基本的规范。但当业务跑了一段时间后,团队已经“敏捷”不起来了,跟不上业务的速度了,我们开始着手做一些基建帮助大家提效,但此时已经不好下手,各个小组用的框架、组件都不相同,只能先从规范统一开始做起。

有哪些规范

按照规范作用划分主要有:流程规范、编码规范、业务规范

如何推动规范统一

制定规范比较容易,但要落地却很难,因为需要对大家有种种约束,如果处理不当容易造成团队反噬,所以我们采用策略是轻拿快放。推进要温柔,多和团队同学提前沟通,多听取建议,落地要干脆, 对于已经达成一致结论,要结合工具和流程快速落地,阶段reivew落地情况。具体推进过程一般遵循以下三条原则:

规范制定流程标准化, 一般先由规范发起人发起提案(以社区或公司已有规范为模版)-> 团队评审->公示(一到两周)->修改->发布正式版本

落地过程渐进化, 规范落地以增量项目优先,规范从严,存量活跃项目(最近双月有无迭代)渐进接入,非活跃项目最后接入或不接入。

执行过程工具化、运营化,对编码类规范进行卡点工具建设,结合发布流水线中进行规范检测拦截,搭建数据大盘,掌控落地情况。非编码类规范要保持前高后低的运营策略,落地前期(1-2个月)保持高频review,中后期力度递减,定期组织bad case 学习。

通过这一系列的规范建设,已经能解决我们研发过程中很多问题了,我们也在其中发现一个道理:真正的“敏捷”不是快,追求快的结果也不一定是快,一切还是需要遵循事物发展的客观规律“做大做强必须先夯实基础”。

团队规范建设中非常重要的一环就是“统一技术选型”,又该如何做呢?

基建建设

统一技术选型

目前底层框架技术选型难度越来越低,各类框架已经越来越趋同,对应社区发展也都很成熟,现在选择更多是基于团队自身沉淀考虑。我们团队早期沉淀以React为主,所以我们团队一开始的技术选型就是围绕React,主要经历三个阶段:自研框架->拥抱社区->回归公司基建:

  1. react全家桶+自定义工程模版+webpack+自定义插件。优点:定制化程度高;缺点:人力投入大,不能稳定迭代,稳定性和扩展性不足;
  2. React 全家桶+ Umi+自定义插件。优点:成熟的解决方案,可靠性很高。缺点:不同插件各个团队重复建设如:容灾插件。
  3. React 全家桶+Eden(字节内部自研)+monorepo。优点:成熟的解决方案,相对定制化,针对公布内部共性问题,提供了统一解决方案。

我们团队在第三阶段技改时,当时团队内部的仓库已经超过100个,活跃迭代的仓库在40+,其中一个全栈项目有15个仓库,当时维护成本很高,过于分散的仓库也不利于团队技术沉淀及改造,所以我们按照业务分类最终合并成了6个monorepo仓库。

研发物料沉淀

研发物料(组件、模块、设计资源等)沉淀是一个团队最有效的效率提升手段,我们先后完成了组件、物料库、系统最佳实践三个维度的沉淀。

组件:作为业务团队,我们团队组件建设重心是业务组件及领域组件(微应用组件,包含业务数据、权限等,如购物车),基础组件库我们则是基于社区开源组件antd结合团队设计规范进行的二次开发。目前我们沉淀了30+的业务组件,5+领域组件。

物料库: 同部门共建物料库,包含组件、工具函数、区块等代码资源。

最佳实践: 系统级别的解决方案,针对常用业务场景进行案例沉淀 ,设计、PM可以快速从中找到相应资源参考,研发可以配合cli将代码下载到工程中。

))

中台渲染引擎

在完成设计规范统一和物料沉淀后效率已经有很大提升,但页面配置化才是中台效率的终极核弹。我们得益设计范统一和组件沉淀,自建了交易业务场景下的渲染引擎,实现了90%+ 页面配置化(页面展示、低复杂度的逻辑由配置生成,复杂的业务逻辑额要外编码),开发效率提升60%左右,通常一周的开发量可以缩短到2pd左右。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码interface PageDSL {
label: string
field: string
widget: Component
widgetProps: {
watch: ['others']
callback: (info) => {},
...
},
formItemProps: { //表单描述
span: number
initialValue?: any
tooltip?: React.ReactNode;
fieldKey?: React.Key | React.Key[]
...
}
textOptions: { // 表格、详情描述
type: string
span: number
render?: ColumnType<R>['render'] | 'ellipsis' | 'date' | 'time' | 'amount',
...
}
}

当然页面配置化并不是银弹,并不能解决所有问题,我们目前主要还是依靠它来做页面的渲染,以及低复杂度业务逻辑配置,对于复杂业务我们还是需要配合编码实现,而且我们也认为应该用编码实现,主要原因有两点:

与当前团队能力不匹配:业务系统过于复杂,开发完全配置化的系统成本和能力要求很高,目前当前团队是无法完成。

完全配置化不代表的效率的提升:复杂业务导致配置过于复杂,成本高、难度大,如果叠加业务迭代快,成本会陡增,并不会比编码快。毕竟业务逻辑的配置,其理解成本、维护成本要远远高于直观的代码。

脚手架建设

模版-快速创建能力:结合自己的业务特点沉淀了多套工程模版,核心能力是通过命令快速创建项目、页面、模块。

ProCode:得益于渲染引擎的建设,我们通常的开发流程为开发物料->DSL配置->特殊业务逻辑开发,其中DSL的配置约占开发时间30%,这段代码结构高度类似,我们最后通过ProCode来生成这份配置,减少开发时间。生成代码的过程为:读取swagger接口定义->生成基础配置->设计稿图片识别生成页面配置->模版编译+ATS生成页面代码->本地应用更新。

在实践过程中图片识别过程比较困难,先通过oci识别页面有哪些字段,再通过大模型MobileNetV2 来标注组件类型,但效果不明显,生成配置错误多,粒度也不够细,所以我们引入配置预览流程,最后由人工去兜底这份配置。由于目前收益没有到达预期,我们也在尝试由前端编写Figma组件,为研发提供组件及页面区块信息,再通过组件中的文本信息去匹配接口字段(和后端约定每个字段上注释带有字段名称信息),生成更准确的DSL。

质量平台建设

质量建设是工程化中绕不开的话题,我们在漫长建设过程中走了不少弯路,简单概括下可以分为三个阶段:

分散治理,头疼医头,脚疼医脚:初期我们的主要精力对一些突出问题进行专项治理,这些治理工作更多是一次性,一段时间后又会裂化,且质量建设是多维度,很难通过一两个维度的建设来评价总体的建设效果,缺少全局视野的情报。

集中治理,搭建前端视角的质量大盘: 通过性能建设、监控报警等一系列专项建设,我们搭建了前端视角的质量看板,但由于功能和视野比较单一,主要是前端同学在用,对业务反哺不够。

联合治理,搭建全场景业务治理平台:联合前端、后端、QA、PM、数据团队按照业务线维度搭建统一的治理平台,将各方质量建设做到入口统一、数据口径统一、目标统一。

工作台

我们在解决完研发资产的重点问题后,开始着手解决一些研发流程上痛点:

资产管理难:团队100多个项目,每个项目有n条关联信息,靠文档、人去管理非常难(小马表示看文档很累!!!)

开发流程不够顺畅:开发一个项目到上线,需要打开N个平台,过程割裂,学习成本高。

项目治理难:团队内的合规、优化等技术改造专项很多,通过人力one by one去改,周期长,成本高、过程难把控。

我们通过自建研发工作台较好的解决了这些问题,它的主要功能有:

研发资产数据化、可视化管理: 开发项目和工作台共用一套数据源(项目所有依赖为一条数据),保证项目实时最新,摆脱文档管理陷阱,并通过数据可视化的提高管理效率。

研发流程全联接: 通过工作台将研发人员、研发工具(vscode)、公司基建、团队沉淀等整合,具备研发全流程能力,包含仓库及项目创建、项目开发、部署、监控、多语言管理、业务埋点管理、自动化测试、流水线任务等能力,实现了all in one platform。

智能治理: 团队内的技术专项通过创建治理任务结合治理脚本实现自动治理,治理过程流程化,进度可追踪 。 举个容灾的例子:由任务发起人开发完容灾功能调用治理插件能力(文件操作、git操作、通知、自动化测试等能力),将代码用AST的方式写入到所有业务git仓库中,并生成commit,同时工作台会生成治理任务并推送给业务同学,业务同学在工作台收到任务通知后验收功能,通过后合并代码,任务进度更新,此时就完成了单个项目治理,整体进度也会在专项群里自动定期同步,催办进度较慢的业务方。

)))

创新应用

上述建设更多是研发视角的,那么用户视角又有哪些呢?技术直接下沉到用户侧主要有两块:

首页搭建: 在渲染引擎的沉淀上建设搭建引擎,让用户可以通过拖拽,生成自己想要的首页,实现了千人千面的效果。生成应用过程可以总结为以下公式:

Dashboard = Layout(widget_1, widget_2, widget_n, …)

Widget = Datasource + View + [Filter + ServiceAPI]

)

模版服务: 让业务方可以自己配置邮件、合同等各类模版,摆脱了对研发的直接依赖,提升业务方工作效率。

)

团队技术品牌建设

为了让团队中的技术建设能够持续传承下去,围绕中台、node、移动端技术方向建设了内部的技术品牌站点。

总结

本文主要讲了这几年一个一线团队所遇到过的问题以及从规范建设到实践落地的具体过程,全部是一些内部的沉淀,后续也会按照模块展开讲讲。也希望能和大家一起交流下各自团队遇到问题以及解决方案,互相学习。

最后再补充下这几年做基建的一些个人感受,概括来说就是两个字“难”和“值”。“难”的点一方面是业务压力大,很难有整块时间,另一方面公司基建比较成熟,轮子容易造重复了。“值”的点在于沉淀了一些有价值的东西,提升了团队效率、质量,团队也在这些基础上逐步完成了整个基建的统一。同时我们在建设过程中也总结了一些经验,这里和大家分享下:

保持定力,持续投入: 建设过程中短期可能会遇到各类困难(研发同学压力大、业务很急),但要保持信念,长期看这种投入值得,且时间越久收益也越大。如果遇到是特别特别难的时候,慢一点也没关系,也不要停下来。

敢于尝试,拿得起放得下:短期一些不明确的方向不要怕错,少量投入,快速验证,方向不正确时能果断止损。

以结果为导向, 不自嗨 : 建设过程中关注实际业务收益,多听取各方建议,避免陷入“我认为很有用”的思维怪圈。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

客户要一次加载上千张轮播图如何丝滑地实现?不用第三方(没找到

发表于 2023-12-09

引言

最近在做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图

这不得卡死啊,现成且现代化好用的第三方库没找到

于是又到了我最爱的实现源码环节,核心代码十多行即可

底部有源码

思路

  • 压缩图片
  • 轮播只需要两张,来回交换,用点障眼法就是无缝了

批量压缩

这个用canvas就能实现,于是我写了个HTML来批量压缩

canvas转存图片时,使用jpeg | webp的格式即可压缩,MDN上有

使用canvas.toBlob可以压缩更多空间,这个不是重点,提一嘴而已

image.png

虚拟无缝轮播实现

直接一张动图,清晰明了的解决问题

t.gif

是不是看起来很简单,加载前两张图,当动画结束时,改变移动那张图的src,同时位移

再加个overflow: hidden不就行了吗

e.gif

编码实现

HTML和CSS我就不贴了,这个没什么难度,容器固定大小,子元素排列一行

然后给包装的容器添加个transform即可

下面是用vue3写的,定义了一个imgIndexArr数组装当前要显示的索引

_data为计算属性,根据imgIndexArr自动变化,里面放的就是图片

我们只需要修改imgIndexArr即可实现数据切换

image.png

我们需要在动画完成时改变,即添加ontransitionend事件

当触发next方法,图片滚动停止后,就要执行onTransitionEnd了

定义俩变量,一个代表最左边的图,一个为右边的图

这里根据变量,决定谁会更新src,并且改变left值实现位移,不好描述啊

transform会一直向右位移,left值也是,所以他们会形成永动机

image.png

在HTML里写上他们位移的样式即可自动更新

image.png

Bug

至此,看着已完成,似乎没有任何问题

但是你把页面隐藏了过后,过一会图片全都不见了,我们打开控制台看看为什么

可以看到,left停止更新了,也就是说,onTransitionEnd没有执行

image.png

transitionend在你浏览器隐藏页面时,就会停止执行

这时需要在页面隐藏时,停止执行,执行如下代码即可

1
2
3
4
5
6
7
8
ts复制代码/** 离开浏览器时 不会执行`transitionend` 所以要停止 */
function bindEvent() {
window.addEventListener('visibilitychange', () => {
document.hidden
? stop()
: play()
})
}

这时一定有人会说,你这不能往左啊,没有控制组件啊

如果要往左的话,只需要把两张图轮流交换改成4张图即可

具体逻辑都是差不多的

源码: gitee.com/cjl2385/dig…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

前端开发者需要了解的「设计体系」

发表于 2023-12-04

yu.png

引言

如果你是一个React技术栈,PC中后台的前端开发工程师,一定知道 Ant Design,并且熟练使用Antd的组件进行前端开发。但是你阅读过 Ant Design 的设计体系,并且知道其是如何帮助产品和设计师进行产品设计工作的吗?

假如碰到以下 2 个问题:

  • 你是否经常和设计师、产品就页面交互和展示不合理进行讨论,但是无法有效达成一致?
  • 你是否可以在没有设计师的情况下,实现一个交互体验、UI 界面都还“不错”的后台页面吗?

从上述2个问题的思考延展出一个话题:作为一名前端开发者,需要了解下什么是设计体系

以下知识点都出自于《设计体系:数字产品设计的系统化方法》

设计体系是什么

设计体系为了实现数字产品的目的而组织起来的一套相互关联的模式和共享实践。使用对象是产品经理和设计师。

  1. 产品目的决定了要采用的设计模式。
  2. 模式指的是界面中那些重复的要素:用户流程、交互方式、按钮、文本框、图标、配色、排版、文案,等等。
  3. 共享实践则是我们如何创建、捕获、共享和使用这些模式,尤其是在团队协作时做这些事情的方法。PS:第三点内容不在这期分享范围内,后续有机会,可以结合古茗前端和UE的协同模式和产出结果再分享。

1.1 设计模式

设计模式是一种用于解决特定设计问题的可复现、可复用的方案。

在数字产品设计过程中,设计模式受三部分内容影响。

  • 首先,产品所属的领域及其核心功能影响了功能性模式(functional pattern)
  • 其次,产品的精神(或者品牌——取决于对品牌的定义)也形成了塑造产品调性的模式,也叫做感知性模式(perceptual pattern)
  • 模式还会受平台的惯例影响,例如PC web,IOS, Android

产品与众不同的原因并不在于它所使用的模式的新颖性,而在于这些模式的运用方式,以及它们如何相互配合以实现特定的设计目的。

设计体系的一个主要目标就是“扩展创意方向”。需要将模式明确地阐述并且分享出来。才可以让团队持续可靠地遵循创意方向。

一套相互关联的模式构成了产品界面的设计语言。设计语言在被阐述时,需要确保它可操作、可重现,以一种系统性的方式进行阐述。

1.2 共享设计语言

在一个团队中工作,设计语言就需要在所有参与产品创建的人之间共享。如果不共享语言,团队成员就无法有效地进行共同创造——每个人都对他们想要实现的目标有着不同的心智模型。

不仅要对语言形成共识,还要对语言的用法形成共识,假如仅仅对按钮这个词的含义达成共识是不够的,还需要对不同背景、不同目的下使用按钮的原因和方法形成共识。

随着设计语言变得更加丰富、更加复杂,需要一种高效的方式来对其进行捕获和共享。

在如今的互联网领域,模式库已经成为良好设计体系实践的重要组成部分。

1.3 模式库及其局限性

模式库不仅包含收集、存储和共享设计模式的工具,还包含相应的使用原则和操作指南。

模式库的局限性

模式库只是有助于设计体系变得更加有效的一种工具,它不等价于设计体系。仅凭工具是无法对用户体验产生重大影响的。模式库是对其背后设计体系的反映。如果没有一套连贯的、集成了模式和实践的设计体系作为基础,这样的组件库的影响将十分有限。

只有将模式库作为构建坚实的设计语言的基础,它才会成为强大的设计工具和协作工具。

1.4 构建有效的设计体系

如何衡量设计体系的有效性?可以看它的不同部分一起发挥作用以帮助实现产品目的的程度。

只有当设计体系围绕产品目的,综合了设计过程中的成本效益及用户体验的效率和满意度时,它才是有效的。

在高效的设计体系中,不同的子体系为了同样的目的而相互连接,并协调一致:

  • 设计方法在前端架构中得到反映;
  • 设计模式遵循设计原则;
  • 模式语言在设计、代码和模式库中得到一致的应用。
  • 不符合设计体系的裂缝总是很容易被发现。割裂的设计体系导致了割裂的用户体验,以及充满矛盾的界面。

产品设计的工作流程更高效,用它们创造出的用户体验更有意义,也更连贯。

1.5 案例:“十分钟食谱”网站

背景:有这样一群人,他们喜欢健康的食品,但又不想花太多的时间做饭。想象一下我们正在开发一个为这类人提供食谱的网站

1.5.1 产品目的和价值观

第一件事就是弄清楚用户是谁,他们的目标、需求和动机是什么。这个目的是产品的核心,它应该支撑设计和开发决策。

用一句话来表述这个网站的目的:让人们在十分钟内就能烹饪出美味又健康的膳食。

1.5.2 设计原则

为确保我们所做的所有事情都清晰地体现了产品目的,我们需要建立一些基本原则和价值观。最重要的是参与产品创建的人都认同这些价值观,并承诺实践它们。

例如,十分钟食谱网站团队的每个人都需要了解时间的价值。

1.5.3 行为和功能性模式

我们确定了希望用户实现或让用户有能力实现的主要行为,这会帮助用户实现他们的目标。设计的细节固然会随着网站的改版而变化,但上述关键行为将始终保持不变。这些行为足以体现网站的核心功能模块和交互方式

1、我们希望人们在十分钟之内就能做出好菜。这意味着我们的食谱只需要简单几步就能完成。这些步骤应该简短、清晰且重点突出。兴许我们还可以为每一步标注时间,确保总时长在十分钟以内。

2、我们希望人们的烹饪行为是自发的,仿佛随时都可以开始。他们可以利用现有的材料,而不必准备什么——他们不需要购买不常见的食材。在用户界面层面,这意味着需要使用带有清晰标注的食材缩略图。

1.5.4 审美和感知性模式

我们需要弄清楚,我们希望人们在使用我们的产品有什么感受。

  • 从思考品牌入手。
  • 我们可以将一些情绪和感觉写下来,并开始尝试一些视觉效果,直到品牌的感觉符合预期。
  • 我们便可以开始定义核心视觉品牌元素,例如排版、配色、说话的语气语调以及任何表现品牌独特性的元素。如插图、图片样式、特定形状、独特交互等,这些都能体现我们服务的本质,并能以最佳方式呈现内容。

1、我们对健康的食品充满热情,所以希望整个网站都能体现这一点。或许网站要有一种有机、温暖、有益健康的感觉。我们也相信烹饪不需要进行大量的规划和准备,它应该是自发的、有趣的,可以在十分钟内进行试验和创新。

2、最终,我们有了温暖又朴实的配色、手绘的图标、可读性极强的排版、高质量的健康饮食照片,以及一些简单的界面元素和控件。这些风格样式便构成了我们的感知性模式。

设计原则

设计原则指的是包含团队如何理解好设计之精髓,以及如何体现这一精髓之建议的共享指南。 换句话说,就是团队或产品,对于什么是好的设计,有一致的标准。

2.1 有效设计原则的特性

2.1.1 设计原则是真实而贴切的

弄明白”设计原则“对团队和产品究竟意味着什么,才是对做设计决策有帮助的。

创新究竟需要什么?什么情况下设计会被认为是有用的?如何判定设计是否真的令人愉快?

好的设计原则所定义的特性能以不同方式解读,但需要将其放在特定的产品背景之下。

2.1.2 设计原则是实用的、可操作的

设计原则应该就如何帮产品解决设计问题提供实用的指导。只有将原则与应用原则的真实案例结合起来,才是最为实际的做法。

  • bad case: 简化,让产品尽可能简单,简单到几乎感觉不到它的存在!我们应该努力消除平台上的摩擦,为用户创造一种可自由地获取内容的体验。如果我们的平台让人很容易理解,那么人们就会更多地使用它。
  • good case: 消除无用的部分。每一个设计元素,无论大小,都必须有一个目的,并遵循它所属元素的目的。如果你无法解释一个元素为什么要在那里,那么它便很可能不应该在那里。

2.1.3 设计原则是有观点的

设计体系应该有态度、有观点,能让每一个使用它的人都得到创意上的指导。

设计是由我们所做的选择所塑造的。即使需要考虑一些相互冲突的因素,好的设计原则,会承认价值观的冲突,也能帮助确定优先级和平衡点,并给出平衡它们的建议。

2.1.4 设计原则是能产生共鸣、容易让人记住的

团队成员都将它们作为日常工作的基础,用于制定设计决策。持续使用的原则才容易让人记住。

它们应当在日常对话中经常被提及,在演示文档和设计批评中经常出现,在一切能用到它们的地方显示出来。

2.2 定义原则

2.2.1 从目的开始

设计原则必须遵循产品的目的,传递产品的精神。

2.2.2 寻找共识

团队需要形成产品设计的原则共识。

2.2.3 面向正确的受众

首先为设计自身和同事而写,尤其是设计师、开发人员、内容编辑、营销专家、领域专家,也就是与产品的创建直接相关的人员。

设计原则的目标应该是就“什么样的设计对产品来说是好设计”这一问题达成粗略的共识,并提供如何实现共识内容的实操指南。

2.2.4 对原则进行测试和修订

随着产品不断演进,设计原则也会不断发展。有可能设计原则在一开始非常宽泛和抽象,但随着时间的推移逐渐变得清晰和具体。也有可能它们一开始是明确的且重点突出,但随着时间的推移逐渐变得空泛并失真。

为了持续改进设计原则,就需要不断地对其进行测试、评估和改进。只有在日常工作中使用它们,并有意识地这样做,设计原则才得以持续改进。

2.3 从原则到模式

以下问题影响着模式的选择与运用。

  • 如何将高层次的概念,如设计原则和品牌价值,物化为具体的用户界面元素。
  • 这些概念究竟是如何体现在我们所创造的模式之中的呢?

原则和模式始终不断地相互影响,相互完善。

  • 设计模式由产品运作方式的核心理念所决定。
  • 模式的选择与运用以及对模式的独特组合,都受产品的目的、精神和设计原则的影响。
  • 可以将设计原则视为创建模式并以具有内在意义的方式组合它们的语法规则。
  • 同样,随着品牌和功能性模式的发展和完善,它们反过来也影响了设计原则。

3、功能性模式

功能性模式是界面中有形的构件。它们的目的是让用户能够完成某种行为,或者激励用户完成某种行为。

3.1 功能性模式演变,行为不变

它们在很大程度上是由产品所属的领域决定的。随着产品的发展,模式也在不断发展。它们的执行、内容、交互方式和显示效果可能会变,但它们所鼓励的核心行为保持相对稳定,因为这是植根于产品目的及其工作原理的。

牢记关键功能性模式的目的,有助于了解设计体系的运转方式,防止设计体系在发展过程中碎片化。

3.2 定义功能性模式

3.2.1 创建模式映射

为了确定客户的需求、目标和动机

  1. 可能做了客户体验映射、JTBD或其他一些类似的围绕客户旅程展开的实践。这些做法的产出物则成了早期设计探索和原型设计的基础。
  2. 专注于界面,有时就会陷入细节之中。我们花费大量时间设计令人印象深刻的页眉,却忘了它的作用,忘了它在用户轨迹的不同阶段是如何影响用户的。换句话说,我们没有将用户行为与鼓励或支持这些行为的确切的功能性模式关联起来。
  3. 每个部分的作用及其所鼓励的行为, 要放眼全局:了解系统的各个部分以及它们是如何相互协作的。

将功能性模式理解为具有相同目的的多个系列,而非单个页面。哪些行为是在用户轨迹的当前阶段需要去鼓励的?哪些模式可以支持这些行为?网站上还有哪些地方有这些模式,以及它们在那些地方是如何运转的?一个新的模式如何服务于整个设计体系?思考上述这些问题,是系统性地开展设计工作的一部分。

3.2.2 打造界面清单

Brad Frost提出的界面清单流程已经成为一种流行的界面模块化方法。

  • 首先,可以把界面打印到纸上,或者将它们放入Keynote或PowerPoint。
  • 然后,便可以将各种不同的组件剪下来,或者在Keynote或PowerPoint中通过剪切、粘贴的方式将不同的组件区分开来
  • 最终,这些组件形成了不同的分类——导航、表单、标签页、按钮、列表等。

通过这一过程,能看出哪些模式是重复的,并发现需要留意的问题区域。当发现有几十个页眉或弹出菜单,便会开始思考如何构建规范。

界面清单不一定包含所有的内容(尽管所做的第一个清单应当是全面的)。它可以一次仅关注一组模式,如促销模块、页眉或者所有产品展示模块,也可以专门针对排版、配色或动画等制作界面清单。

为了保持最佳效果,应该定期维护界面清单。即便团队已经维护了一个模式库,新的模式也会出现,需要放入整个设计体系。如果每隔几个月就维护一次界面清单,每次做这项工作就只需要花几个小时而已。而且,每次这样做的时候,都会让团队更好地理解设计体系并改进它。

3.2.3 将功能性模式视为操作

要理解一个功能性模式的目的,需要关注它的作用是什么,而不是你认为它是什么。

  • 要试着找到最能描述其行为的操作。用动词而非名词来描述模式,有助于扩展模式的潜在用例,并更准确地定义其用途。
  • 当你试着描述它是什么的时候,就过于局限于其呈现和内容了,最终容易将模式的使用范围限制在特定的上下文中。
  • 如果根据操作——从用户角度及你自身的角度——来定义模式,便可以发现它的目的。通过关注操作,可以将模式与行为联系起来,并兼容各种不同的用例

3.2.4 描绘模式的内容结构

要对功能性模式的工作方式达到共识,请描绘其结构:让功能性模块能够有效运行的核心内容。内容结构与模式的目的是密切相关的。

  1. 列出让模块能够有效运行的核心内容元素
  2. 试着确定元素的层次结构,并决定如何对它们进行分组。
  3. 对功能性模式的结构有了共识,形成产品功能性设计原型
  4. 设计师负责视觉上的探索,而开发人员则开始整理原型(或者两者都可以做原型,这取决于具体的工作方式)。设计师知道在模式的不同阶段,他们可以将视觉设计推进到什么程度。开发人员了解设计选择的原因,并且不会让意料之外的设计被扔到墙上。

3.2.5 按某个维度排列模式

可以试着将相似的模式按照某个维度排在一起, 进行模式库的设计和提炼。

在某个维度上排列模式有助于确保对它们的使用是恰当的,不会在整个系统中争夺注意力。

这样做还有助于防止不必要地重复创建模块。

3.2.6 将内容视为假设

从目的开始,而不是从内容开始。这样,我们便不会将内容视为已知的资源,而是视为一种假设(hypothesis)。这样做能检测出我们是否已经定义好了模块的目的,以及我们的设计是否符合这一目的。

如果内容总是不能匹配此功能性模式,通常是由以下三个原因中的一个或多个引起的。

  • 我们没有正确地定义模式的目的。请试着重新去理解模式是用于支持哪些行为的。
  • 我们设计模式的方式并不是最能反映其目的的方式。请为此模式尝试其他的设计。
  • 我们试图将内容强行放入不合适它们的模式。请考虑修改内容,或尝试其他模式。

3.3 总结

产品目的决定了其他的一切:模式的结构、内容及呈现。

了解模式的目的(也即了解需要达成或激励的行为),可以帮助我们设计和构建更加稳健的模块。

这样做可以帮助我们知道一个模式在应用之前有多少可以修改。这样做为整个团队提供了一个共同的参考点,并将模式与原始的设计意图联系在一起,从而减少了重复的情形。牢记目的还会让检测模式的有效性变得更加容易。

4、感知性模式

如果说功能性模块反映的是用户需要且想要的内容,那么感知性模式关注的则是他们直观的感受或行为。感知性模式并非来自用户的行为和操作,而是产品尽力打造的个性和营造的氛围带来的产物。

数字产品感知性模式的例子包括语气、排版、配色、布局、插图与图标样式、形状与纹理、间距、意象、交互或动画,以及这些要素在界面中的组合和使用的具体方式。

4.1 感知性模式的作用

4.1.1 感知性模式有助于传递品牌形象

感知性模式通过界面传递品牌,并让品牌被人记住。

4.1.2 感知性模式让系统更为连贯

在模块化的系统中,想要做到视觉上的连贯统一可能是一件很棘手的事情。

模块是由不同的人根据不同的目的创建的。而由于感知性模式是渗透到系统中各个部分的,因此它们可以将系统的不同部分连接起来。如果这种连接是有效的,那么用户就会感受到模块之间的统一性。

4.2 探索感知性模式

举例一些模式

4.2.1 情绪板

情绪板是探索不同视觉主题的绝佳工具,定义大致的品牌方向。

4.2.2 样式叠片

定义好了大致的品牌方向之后,便可以使用样式叠片来更加细致地探究多种可能性。

样式叠片其定义为 ”由字体、颜色等界面元素组成的能传递Web视觉品牌精髓的设计交付物。“

4.3 迭代与改进

将样式集成到产品的过程中,样式的演变还将持续。在更为真实的界面设置中,使用模块和交互,进行品牌上的尝试,对感知性模式和功能性模式的改进都是有益的。

这是典型的迭代过程,不同的模式相互影响,直到最终设计语言得以成形。

4.3.1 平衡品牌性与一致性

产生太多的例外会削弱品牌性,同样,过分关注一致性也会扼杀品牌性。矛盾在于,让设计达到完美的一致性无法确保它依然具有很强的品牌性。有时,这样做反而会削弱品牌性——在一致性和统一性之间存在着细微的差别。

发展感知性模式需要为设计师赋予打破常规的权力,鼓励设计师积极地探索更多的可能性。好的设计体系能在品牌的一致性和创造性表达之间取得平衡。

4.3.2 标志性时刻

小的细节可以为用户体验增加深度和意义。当我们为设计的系统化和结构化努力的时候,需要留意那些能让产品与众不同的细节。在构建设计体系的时候,一定要为创造和发展这些时刻而留足空间。

4.3.3 小规模试验

探索新样式的时候,请先在网站的一小块区域进行试验。打破常规的时候要格外小心,留意那些设计体系之外的模式,以及尝试这些模式的原因。如果它们起作用,就将它们应用到网站的其他区域,逐渐将它们融入设计体系。

4.3.4 平衡品牌和业务需求

无论我们如何保护品牌,这些事情都会发生——新的需求需要定制化的模式以及一次性的调整。如果我们没有意识到这些,那么这些例外之处就有可能削弱品牌性。

总结

  • 目的:设计体系的目的是帮助实现产品的目的,设计体系中的所有内容——大到团队运作方式,小到最小的模式——都应该为了实现这些目标进行优化。
  • 原则:团队在设计时需要判断如何实现产品的目的。其设计方法和优先级的选择应当基于一系列原则,团队对原则的认识越一致,他们创造的模式就越有凝聚力。
  • 模式:我们打造的界面旨在帮助人们实现某种目标并创造某种感受。我们的设计意图是通过设计模式表现出来的。功能性模式支持用户的行为和操作,感知性模式关注的是产品给人的直观感受。

最后

📚 小茗文章推荐:

  • 门店智能设备间「通信」原理
  • 「前端添加水印」你真的了解全面吗?
  • 一文了解Webpack中Tapable事件机制

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android PLT-GOT hook 实现

发表于 2023-12-01

简介

之前在 ELF中.got,.plt section的作用、lazy binding的实现及全局符号介入的影响 一文中提到:对于position independent code ,非static的函数调用是间接进行的,会先调用目标方法对应的一个 plt 跳板函数,然后跳板函数再跳转到对应 got 表项中真实目标函数的地址,实现目标方法的调用的。

备注:

  1. 对于调用当前库自己的非 static 函数,如果其 visibility 是 hidden 或者 protected 的话,是不会有上述间接流程的
  2. 上面没有提lazy binding的流程,因为 plt hook 跟他关系不大,在后面提 invoke original method 实现时会提一下

从上面的间接调用流程可知:如果我们把对应 got 表项中目标函数地址修改为我们的代理函数地址,那么后续的调用就会进入我们的代理函数,从而实现了hook。PLT-GOT hook 也就这么做的,下面我们看下具体实现。

ELF 加载地址

上面提到要hook某个库对某个函数的调用,就要修改它相应的got表项,修改got表项的前提是我们能找到它在内存中的地址,进而我们要先找到该库(ELF)在内存中的加载地址。

常见的查找 elf 文件加载地址的方法有:

  1. 借助 dl_iterate_phdr
  2. 解析 maps

通过dl_iterate_phdr查找elf加载地址

简单示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c++复制代码static int dl_callback(dl_phdr_info* info, size_t size, void* data) {  
auto arg = (std::pair<const char*, uint64_t>*)data;
string libName = arg->first;
string dlname = info->dlpi_name;
if (!std::equal(libName.rbegin(), libName.rend(), dlname.rbegin())) {
return 0;
}
if (dlname.size() > libName.size() && *(dlname.rbegin() + libName.size()) != '/') {
return 0;
}
arg->second = info->dlpi_addr;
return 0;
}

static uint64_t getLibLoadAddrWithDlIteratePhdr(const char* libName) {
std::pair<const char*, uint64_t> arg = {libName, 0};
dl_iterate_phdr(dl_callback, (void*)&arg);
return arg.second;
}

解析maps获取elf加载地址

elf 文件是通过 mmap 映射到内存中的,而 /proc/${pid}/maps 中包含了所有的内存映射信息,因此通过读取并解析 maps 就可以获取 elf 的加载地址。

先来看下 maps 的格式:

1
2
3
4
5
6
7
8
9
10
bash复制代码address               perms offset  dev   inode  pathname
...
35b1800000-35b1820000 r-xp 00000000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a20000-35b1a21000 rw-p 00020000 08:02 135522 /usr/lib64/ld-2.15.so
35b1c00000-35b1dac000 r-xp 00000000 08:02 135870 /usr/lib64/libc-2.15.so
35b1dac000-35b1fac000 ---p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so
35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so
35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870 /usr/lib64/libc-2.15.so
...
  1. 最后一列是被映射到内存的库的路径(如果是文件映射的话),通过字符串匹配可以筛选出要查找库的相关条目
  2. 每个库可能会有多个映射段,寻找到 offset 为 0 的那一段
  3. 第一列是该映射区段的起始-结束地址,起始地址即是该库的加载地址

备注:
上面提到”寻找 offset 为 0 的那一段“,通常是能找到的,不过并不能保证。
通常 elf file header 会放到 第一个 loadable segment 中,因此通常 第一个 loadable segment 的file offset为0,那么mmap映射的时候,第一个map region的offset就是0,由此它的start address处就是elf file header在内存中的映射位置,方便我们解析。但是通过linker script我们可以将elf file header排除在 loadable segment之外,还可以对segment做更多调整,因此offset可能不为0。

解析ELF

上面我们拿到elf文件的加载地址后,就可以解析 elf file header 来找到 program header table的位置。然后遍历 program header table:

  1. 找到第一个 loadable (PT_LOAD)的segment,根据它的p_vaddr 来计算出 elf base virtual address:load_bias,后续 elf 中各个部分在内存中的虚拟地址就需要通过 load_bias 加上它们的 p_vaddr获得
  2. 找到 dynamic (PT_DYNAMIC)segment,后续解析 dynamic segment 就可以知道动态符号表(.dynsym),动态字符串表(.dynstr),哈希表(.hash,.gnu.hash),重定向表等等重要信息

ELF base virtual address的计算

elf虚拟基地址的计算方法可以参考Android bionic linker加载segment的实现逻辑:ElfReader::ReserveAddressSpace & ElfReader::LoadSegments

不过更简单直接的方式是看它的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c++复制代码/**
However, in practice, segments do _not_ start at page boundaries. Since we
can only memory-map at page boundaries, this means that the bias is
computed as:
load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr)
(NOTE: The value must be used as a 32-bit unsigned integer, to deal with
possible wrap around UINT32_MAX for possible large p_vaddr values).
And that the phdr0_load_address must start at a page boundary, with
the segment's real content starting at:
phdr0_load_address + PAGE_OFFSET(phdr0->p_vaddr)
Note that ELF requires the following condition to make the mmap()-ing work:
PAGE_OFFSET(phdr0->p_vaddr) == PAGE_OFFSET(phdr0->p_offset)
The load_bias must be added to any p_vaddr value read from the ELF file to
determine the corresponding memory address.
**/

这里明确给出了 load_bias 的计算方法:

1
c++复制代码load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr)

而且也指出后续 elf 中各个部分在内存中的虚拟地址需要通过 load_bias 加上它们的 p_vaddr获得

解析dynamic segment

为了实现函数hook,我们就需要根据函数名找到对应的符号,因此就需要哈希表,动态字符串表,动态符号表。

为了能修改函数对应got项中的地址,就需要先找到其got项的位置,因此需要重定位表的信息。

而这些信息在 dynamic segment 中都能获取到,解析后可以得到类似如下的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
c++复制代码struct Dynamic {
// plt relocation section info (eg: .rela.plt)
ElfW(Addr) pltRelSecAddr;// DT_JMPREL
ElfW(Xword) pltRelSecSize;// DT_PLTRELSZ

// dynamic 其他部分的重定位信息 (eg: .rela.dyn)
ElfW(Addr) relSecAddr;// DT_RELA or DT_RELA,重定位表的地址
ElfW(Xword) relSecSize;// DT_RELASZ or DT_RELSZ,重定位表的大小
ElfW(Xword) relSecEntrySize;// DT_RELAENT or DT_RELENT,重定位表 表项大小

// plt 对应的 got section info (.got.plt)
ElfW(Addr) gotPltSecAddr;// DT_PLTGOT

// 动态字符串表信息 (.dynstr)
ElfW(Addr) dynstrSecAddr;// DT_STRTAB,动态字符串表地址
ElfW(Xword) dynstrSecSize;// DT_STRSZ,动态字符串表大小

// 动态符号表信息 (.dynsym)
ElfW(Addr) dynsymSecAddr;// DT_SYMTAB,动态符号表地址
ElfW(Xword) dynsymEntrySize;// DT_SYMENT,动态符号表 表项大小

// hash section addr
ElfW(Addr) hashSecAddr;
ElfW(Addr) gnuHashSecAddr;

bool isRela;
bool bindNow;
};

备注:此处忽略了 relr 类型的重定位表

查找函数符号

根据函数名,哈希表(.hash,.gnu.hash),动态符号表(.dynsym),动态字符串表(.dynstr)就可以查找到目标符号,具体查找方法可以参考:ELF 通过 Sysv Hash & Gnu Hash 查找符号的实现及对比

查找目标got项的位置

在 ELF中.got,.plt section的作用、lazy binding的实现及全局符号介入的影响 一文中提到:对于非 static 函数的调用,因为符号地址在编译时未知(外部库的符号地址未知很好理解,同一个库的非static符号地址未知是因为全局符号介入的影响),运行时由动态链接器将符号地址填入对应的got项中,这样函数调用的时候就能从got项中找到目标函数的地址以跳入执行。

那么动态链接器是怎么知道某个符号对应的got项在哪儿呢?因为有重定位项信息,我们来看个具体例子:

1
2
3
4
5
6
sql复制代码Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
...
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.17 (2)
...
1
2
3
4
5
sql复制代码Relocation section '.rela.plt' at offset 0x440 contains 3 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
...
0000000000020008 0000000500000402 R_AARCH64_JUMP_SLOT 0000000000000000 malloc@GLIBC_2.17 + 0
...

对于aarch64架构,plt 重定位项的类型一定是:R_AARCH64_JUMP_SLOT,其info中有对应符号的索引,从上面的例子来看 symbol index = ELF64_R_SYM(0x0000000500000402) 正好是 5,跟上面动态符号表中的一致,而其 offset + load_bios 正是目标 got 项的位置。

一个简单实现例子:

1
2
3
4
5
6
7
8
9
10
11
c++复制代码ElfW(Addr) Elf::findSymbolRelAddr(uint32_t symbolIdx) const {   
auto start = (const ElfW(Rela)*)dynamic_.pltRelSecAddr;// rela for example
auto end = start + dynamic_.pltRelSecSize;
for (; start < end; ++start) {
if (ELF64_R_SYM(start->r_info) == symbolIdx) {
assert(ELF64_R_TYPE(start->r_info) == R_AARCH64_JUMP_SLOT);
return loadBias_ + start->r_offset;
}
}
return 0;
}

实现hook

写权限

上面已经找到目标 got 项的位置,因此我们向其中写入代理函数的地址,hook就完成了。

对于延迟绑定的情况来说,一般 .got.plt所在内存确实是有写权限的,因为随着代码的执行,可能不断有函数第一次被调用,需要动态链接器去查找并写入其地址。但是如果是加载时立即绑定的情况(Android arm架构就是非延迟绑定的情况),链接器在加载动态库时就会完成所有符号的绑定,因此通常会将 .got.plt 设置为只读模式。

因此在写入代理函数地址之前,需要先判断下是否有写权限,这个可以通过读 maps 来得知。如果没有写权限的话,可以通过 mprotect 先赋予写权限:

1
2
3
c++复制代码if (mprotect((void*)PAGE_START(symbolRelAddr), PAGE_SIZE, m->perms() | PROT_WRITE) != 0) {
// todo handle error
}

原函数地址

通常在代理函数里面需要调用原函数,hook 方法通常需要返回原函数的地址。那么如何获取原函数的地址呢?

  1. 如果是立即绑定的case,那么对应got项中的值就是原函数的地址,在写入代理函数地址前先将其读出即可
  2. 如果是延迟绑定的case,hook前原函数可能还没有调用过,此时got项中保存的是plt中的跳板函数地址,以跳入动态链接器的符号查找函数,这种情况可以通过dlsym来查找原函数地址

那么如何判断是立即绑定还是延迟绑定呢?(Android arm架构其实不用判断,都是立即绑定的)

  1. 如果动态库 .dynamic section中存在 DT_BIND_NOW 项,那么会立即绑定
  2. 如果动态库 .dynamic section中DT_FLAGSvalue设置了DT_BIND_NOW或者DT_FLAGS_1value设置了DF_1_NOW的话,会立即绑定
  3. 如果运行时环境变量包含LD_BIND_NOW,会立即绑定

调用原函数

有了原函数地址后,调用原函数就简单了:将其cast成原函数类型直接调用就行。也可以定义一个简单的宏来简化使用,比如:

1
2
3
4
5
6
7
8
c++复制代码#define INVOKE_ORIGINAL(func, addr, ...) (((decltype(func)*)addr)(__VA_ARGS__))

void* my_malloc(size_t size) {
LOGI("my_malloc invoked with size: %zu", size);
void* ptr = INVOKE_ORIGINAL(my_malloc, malloc_fun_addr, size);
LOGI("malloc res: %p", ptr);
return ptr;
}

清除指令缓存

CPU有指令缓存,我们修改got项后还需要清除一下,使得CPU重新取指:

1
c++复制代码__builtin___clear_cache((char*) PAGE_START(symbolRelAddr), (char*) PAGE_END(symbolRelAddr));

Hook 所有库中某个函数的调用

有些情况下需要hook所有库中对某个函数的调用,这个时候有个麻烦的地方:有些库是在 hook 方法调用之后才被加载的,那怎么能hook到它呢?一种方法是我们在内部hook所有库的 dlopen 方法,这样后续加载库的时候我们hook框架能立马感知到,然后对新加载的库进行hook。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

git 如何撤回已push的代码

发表于 2023-11-30

idea如何撤回push到远程的代码 – 编程屋 (rivers-all.com)

Idea:撤销git已经push的代码(完美解决)_idea回退已经push的代码_IT枫斗者的博客-CSDN博客

使用IDEA新建、合并、删除分支。_idea删除分支_胡亥。的博客-CSDN博客

在日常的开发中,我们经常使用Git来进行版本控制。有时候,我们可能会不小心将错误的代码 Push 到远程仓库,或者想要在本地回退到之前的某个版本重新开发。

或者像我一样,写了一些感觉以后很有用的优化方案push到线上,又接到了一个新的需求。但是呢,项目比较重要,没有经过测试的方案不能轻易上线,为了承接需求只能先把push上去的优化方案先下掉。

现在我的分支是这样的,我想要在本地和远程仓库中都恢复到help文档提交的部分。

image.png
1.基础的手动操作(比较笨,不推荐)


这样的操作非常不推荐,但是如果你不了解git,确实是我们最容易理解的方式。

如果你的错误代码不是很多,那么你其实可以通过与你想要恢复到的commit进行对比,然后手动删除错误代码,然后删除不同的代码。

image.png

按住 ctrl 选择想要对比的两个commit,然后选择 Compare Versions 就能通过对比删除掉你想要删除的代码。

这个方案在代码很简单时时非常有效的,甚至还能通过删除后最新commit和想要退回的commit在Compare一下保障代码一致。

但是这个方法对于代码比较复杂的情况来说就不太好处理了,如果涉及到繁杂的配置文件,那更是让人头疼。只能通过反复的Compare Version来进行对比。

这样的手动操作显然显得有些笨拙了,对此git有一套较为优雅的操作流程,同样能解决这个问题。

  1. git Revert Commit(推荐)

image.png
同样的,我第三次提交了错误代码,并且已经push到远程分支。想要撤回这部分代码,只需要右键点击错误提交记录

image.png
git自动产生一个Revert记录,然后我们会看到git自动将我第三次错误提交代码回退了,这个其实就相当于git帮我们手动回退了代码。

image.png
后续,只需要我们将本次改动push到远程,即可完成一次这次回退操作,

image.png

revert相当于自动帮我们进行版本回退操作,并且留下改动记录,非常安全。这也是评论区各位大佬非常推荐的。

但是revert还是存在一点不足,即一次仅能回退一次push。如果我们有几十次甚至上百次的记录,一次次的单击回退不仅费时费力而且还留下了每次的回退记录,我个人觉得revert在这种情况下又不太优雅。

  1. 增加新分支(推荐撤回较多情况下使用)

如果真的需要回退到上百次提交之前的版本,我的建议是直接新建个分支。

在想要回到的版本处的提交记录右键,点击new branch

image.png
image.png
image.png

新建分支的操作仅仅增加了一个分支,既能保留原来的版本,又能安全回退到想要回退的版本,同时不会产生太多的回退记录。

但是此操作仍然建议慎用,因为这个操作执行多了,分支管理就又成了一大难题。

  1. Reset Current Branch 到你想要恢复的commit记录(不太安全,慎用)

image.png

这个时候会跳出四个选项供你选择,我这里是选择hard。

其他选项的含义仅供参考,因为我也没有一一尝试过。

  1. Soft:你之前写的不会改变,你之前暂存过的文件还在暂存。
  2. Mixed:你之前写的不会改变,你之前暂存过的文件不会暂存。
  3. Hard:文件恢复到所选提交状态,任何更改都会丢失。

你已经提交了,然后你又在本地更改了,如果你选hard,那么提交的内容和你提交后又本地修改未提交的内容都会丢失。
4. keep:任何本地更改都将丢失,文件将恢复到所选提交的状态,但本地更改将保持不变。
你已经提交了,然后你又在本地更改了,如果你选keep,那么提交的内容会丢失,你提交后又本地修改未提交的内容不会丢失。

image.png
image.png

image.png

然后,之前错误提交的commit就在本地给干掉了。但是远程仓库中的提交还是原来的样子,你要把目前状态同步到远程仓库。也就是需要把那几个commit删除的操作push过去。

打开push界面,虽然没有commit需要提交,需要点击Force Push,强推过去。
image.png

需要注意的是对于一些被保护的分支,这个操作是不能进行的。需要自行查看配置,我这里因为不是master分支,所以没有保护。

image.png
可以看到,远程仓库中最新的commit只有我们的help文档。在其上的三个提交都没了。

image.png
注意:以上使用的是2023版IDEA,如果有出入的话可以考虑搜索使用git命令。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

额,收到阿里云给的赔偿了

发表于 2023-11-29

众所周知,就在刚过去不久的11月12号,阿里云突发了一次大规模故障,影响甚广。

以至于连咱们这里评论区小伙伴学校的洗衣机都崩了(手动doge)。

这么关键的双11节点,这么多热门业务和产品,这么大规模的崩盘故障,不一会儿这个事情便被推上了热搜。

而就在近日,阿里云官网上就该故障也给出了一份故障复盘报告,而报告中则给出了这次事件的问题原因。

细看一下不难发现,说到底,在代码级还是存在逻辑缺陷问题。当然阿里云在报告中也给出了一系列相应的改进措施:

  • 增加AK服务白名单生成结果的校验及告警拦截能力。
  • 增加AK服务白名单更新的灰度验证逻辑,提前发现异常。
  • 增加AK服务白名单的快速恢复能力。
  • 加强云产品侧的联动恢复能力。

其实当时发生这个事情时,正好是周日的傍晚,当时自己正在家里吃晚饭,所以对于这波故障的直接感受并不明显。

本来对这个事情都没太注意了,不过就在前几天,突然收到了一条来自于阿里云的赔偿短信。

出于好奇,我也登进阿里云的控制台尝试领取了一下。

果然,50很快就到账了(不过是代金券。。)。

而赔偿对象则为阿里云的对象存储OSS服务。

看到这里我才想起来,因为之前自己用的阿里云对象存储OSS来存东西,所以收到这条赔偿短信也就不奇怪了。

不过,它这条短信里所谓的SLA赔偿到底是按照什么标准来的呢?

同样出于好奇,我也看了一下阿里云SLA定义与详细规则。这次的赔偿也是按照不同产品的服务等级协议来划分的。

比如我这次受影响的的使用产品就是阿里云的对象存储OSS,而其对应产品的服务等级协议里也明确规定有具体的赔偿标准。

后台显示当时对象存储OSS的服务可用性为99.9884%。

按照阿里云承诺的当前产品服务可用性不低于99.99%的标准,很明显这就触发赔偿了。

而具体赔付比例按照上面产品服务等级协议里的描述,则来到了10%这个档。

看到这里,我也不禁想起了前段时间语雀的故障赔付,当时语雀的补偿方案是针对个人用户赠送6个月的会员服务。

对于这样类似的赔偿结果,有的用户表示愿意继续给产品一次机会,当然也有用户会表示无法原谅并弃用之。

其实这种长时间、大规模的故障,对于一些重度依赖云产品的用户或者业务来说打击往往是致命的。而这些事后给出的所谓的SLA内的赔偿和客户实际所承担的业务损失来说往往是杯水车薪,压根就覆盖不住,这还不谈客户为此所额外付出的人力物力成本。

因此对于这些云服务商而言,除了赔偿,更重要的还是多研究研究如何加强故障预防和处理,持续提升服务的稳定性和可靠性才是关键。

注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

🔥🔥Java开发者的Python快速实战指南:探索向量数据库

发表于 2023-11-28

前言

如果说Python是跟随我的步伐学习的话,我觉得我在日常开发方面已经没有太大的问题了。然而,由于我没有Python开发经验,我思考着应该写些什么内容。我回想起学习Java时的学习路线,直接操作数据库是其中一项重要内容,无论使用哪种编程语言,与数据库的交互都是不可避免的。然而,直接操作MySQL数据库似乎缺乏趣味性,毕竟每天都在写SQL语句。突然我想到了我之前写过的一系列私人知识库文章,于是我想到了向量数据库,毕竟这是当前非常热门的技术之一。

如果AI离开了向量数据库,就好像失去了灵魂一样。市面上有很多向量数据库产品,我选择了最近腾讯推出的向量数据库,并且我还有一张免费试用卡,趁着还没过期,我决定写一些相关文章。而且我看了一下,这个数据库对于新手来说非常友好,因为它有可视化界面。对于一个新手来说,能够看到实际效果是最客观的。就像当初学习SQL时,如果没有Navicat这个可视化工具,就会感觉力不从心一样。

向量数据库

向量数据库具有将复杂的非结构化数据转化为多维逻辑坐标值的能力,简而言之,它可以将我们所了解的所有事物转化为可计算的数字。一旦数据进入数学领域,我们就能够对其进行计算。此外,向量数据库还可以作为一个外部知识库,为大型模型提供最新、最全面的信息,以应对需要及时回答的问题。同时,它也能够赋予大型语言模型长期记忆的能力,避免在对话过程中产生”断片”的情况。可以说,向量数据库是大型语言模型的最佳合作伙伴。

如果你对任何内容有任何疑问,请点击以下官方文档链接查看更多信息:img-bss.csdnimg.cn/1113tusoutu…

虽然这是官方的文档,里面存在许多错误,我已经积极提供了反馈,但可惜没有得到有效处理。尽管如此,这并不会妨碍我们的阅读。文档最后还有一个官方的案例代码仓库,对于有兴趣的同学可以直接滑动到最后进行查阅。不过,对于新手而言,可能并不太友好,原因在于代码量较大,很难一下子消化。就好比刚学习Java的时候,要看别人的业务逻辑一样,即使有大量注释,也会感到吃力。

好的,废话不多说,我们直接进入正题吧。如果你还有未领取的,可以免费领取一下。

腾讯官方体验地址:cloud.tencent.com/product/vdb

建立数据库连接

领取完毕后,你需要创建一个新的免费示例,这个过程不难,大家都会。成功之后,你需要开启外网访问,否则无法进行本地的测试和联调。在开启外网访问时,需要将外网白名单ip设置为0.0.0.0/0,这将接受所有IP的请求。

image

好的,接下来我们需要获取数据库的登录名和密码。这些信息将用于连接和管理数据库。

image

创建数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import tcvectordb
from tcvectordb.model.enum import FieldType, IndexType, MetricType, ReadConsistency

#create a database client object
client = tcvectordb.VectorDBClient(url='http://*******', username='root', key='1*******', read_consistency=ReadConsistency.EVENTUAL_CONSISTENCY, timeout=30)
# create a database
db = client.create_database(database_name='db-xiaoyu')

print(db.database_name)

# list databases
db_list = client.list_databases()


for db in db_list:
print(db.database_name)

好的,我们现在开始替换所需的内容,完成数据库的创建。一旦数据库创建完成,我们还需要创建集合,而不是传统的表,因为在向量数据库中,它们被称为集合。因此,我们接下来要创建集合。

创建集合

创建集合和创建表的过程类似,但前提是集合需要存储向量,而表用于存储数据。在这里,我们选择使用集成了embedding的集合。如果不使用集成的embedding,你需要使用其他embedding模型来输出向量,然后将其输入到集合中进行存储。除非你想手动输入向量值,否则这是必要的。

设计索引(不是设计 Collection 的结构)

在使用向量对应的文本字段时,不建议建立索引。这样做会占用大量内存资源,而且没有实际作用。除了向量对应的文本字段外,如果需要进行业务过滤,也就是在查询时需要使用where条件,那么必须单独为这个条件字段定义一个索引。也就是说,你需要用哪个字段进行过滤,就必须为该字段定义一个索引。向量数据库支持动态模式(Schema),在写入数据时可以写入任意字段,无需提前定义,类似于MongoDB。目前,主键id和向量字段vector是固定且必需的,字段名称也必须一致,否则会报错。

在之前讲解私人知识库的时候,我会单独引入其他embedding模型,因为向量数据库没有继承这些模型。不过,腾讯已经将embedding模型集成在了他们的系统中,这样就不需要来回寻找模型了。需要注意的是,为了确保一致性,你选择的embedding模型后面的vector字段要设置为768维。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码db = client.database('db-xiaoyu')
# -- index config
index = Index(
FilterIndex(name='id', field_type=FieldType.String, index_type=IndexType.PRIMARY_KEY),
VectorIndex(name='vector', dimension=768, index_type=IndexType.HNSW,
metric_type=MetricType.COSINE, params=HNSWParams(m=16, efconstruction=200)),
FilterIndex(name='author', field_type=FieldType.String, index_type=IndexType.FILTER),
FilterIndex(name='bookName', field_type=FieldType.String, index_type=IndexType.FILTER)
)

# Embedding config
ebd = Embedding(vector_field='vector', field='text', model=EmbeddingModel.BGE_BASE_ZH)

# create a collection
coll = db.create_collection(
name='book-xiaoyu',
shard=1,
replicas=0,
description='this is a collection of test embedding',
embedding=ebd,
index=index
)
print(vars(coll))

我们已经成功创建了数据库和集合,并且现在让我们来看一下它们的结构。实际上,它们的原理与MySQL和其他数据库相似,只是存储的内容和术语发生了变化。我们可以将其视为数据库操作。

image

插入/替换数据

当插入数据时,如果集合中已经存在具有相同ID的文档,则会删除原始文档并插入新的文档数据。需要注意的是,很多字段我们都没有指定,例如page、text等。你可以继续添加这些字段,因为它们类似于MongoDB。但请注意,text字段必须与你在配置embedding时指定的字段相同,否则无法将其转换为向量。

1
2
3
4
5
6
7
8
9
10
11
python复制代码coll = db.collection('book-emb')
# 写入数据。
# 参数 build_index 为 True,指写入数据同时重新创建索引。
res = coll.upsert(
documents=[
Document(id='0001', text="话说天下大势,分久必合,合久必分。", author='罗贯中', bookName='三国演义', page=21),
Document(id='0002', text="混沌未分天地乱,茫茫渺渺无人间。", author='吴承恩', bookName='西游记', page=22),
Document(id='0003', text="甄士隐梦幻识通灵,贾雨村风尘怀闺秀。", author='曹雪芹', bookName='红楼梦', page=23)
],
build_index=True
)

当我们完成数据插入后,我们可以立即执行查询操作。但请注意,如果你将 “build_index” 字段设置为 “false”,即使插入成功,查询时也无法检索到数据。因此,如果要立即生效并能查询到数据,你必须将其设置为 “true”。这个是重建索引的过程

查询数据

这里的查询可以分为精确查询和相似度查询两种。精确查询是指除了向量字段外的其他字段查询条件都是精确匹配的。由于我们在建立索引时已经对作者(author)和书名(bookName)建立了索引,所以我们可以直接对它们进行数据过滤,但是我不会在这里演示。现在我将演示一下模糊查询,即对向量字段匹配后的结果进行查询,并同时加上过滤条件。

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码doc_lists = coll.searchByText(
embeddingItems=['天下大势,分久必合,合久必分'],
filter=Filter(Filter.In("bookName", ["三国演义", "西游记"])),
params=SearchParams(ef=200),
limit=3,
retrieve_vector=False, # 不返回向量
output_fields=['bookName','author']
)
# printf
for i, docs in enumerate(doc_lists.get("documents")):
for doc in docs:
print(doc)

除了上面提到的Python的写法,我们还可以通过界面来进行精确查询。只需要在界面中填写where后的条件即可。

image

要进行模糊查询,可以直接使用text文字进行查询,或者定义过滤字段来进行查询优化。

image

总结

剩下的删除数据这部分我就不演示了。今天先跟向量数据库熟悉一下界面操作,感觉就像在使用Kibana查询ES数据一样。不知道你们有没有类似的感觉。好了,今天我们先只关注文本操作,下一期我会尝试处理图像或者视频数据。总的来说,相比Java,Python的SDK使用起来更加舒适。如果你曾经使用过Java SDK与平台接口对接,就会发现Python SDK上手更快。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ELF中got、plt section的作用、lazy

发表于 2023-11-28

动态库重定向

每个进程都可以拥有一个独立的虚拟地址空间,所以对于可执行文件,他可以有一个固定的虚拟基地址,但是对于动态库,为每个动态库划分固定的虚拟地址范围会非常麻烦,而且可能无法做到:比如库的大小变了、要增加新的库、某个库废弃不用了,等等情况都要重新划分。另外当以类似插件的形式运行时要增加一个新库时,无法找到一个固定可用的虚拟地址范围。因为无法预知所以无法事先预留,因此需要动态链接器在运行时进行重定向。本文后面讨论的都是针对position independent code。

GOT

GOT表简介

GOT:Global Offset Table,是用于访问全局变量的。

GOT表中存的是变量的虚拟地址,当访问全局变量时,会先从相应got表项中获取变量的地址,然后再从该地址中读出值,或者向该地址中写入新值。

引入GOT表的优缺点

优点
  1. 引入got表后,重定向时,文本段不需要修改,因此文本段可以在多个进程中共享,可减少内存使用
  2. 引入got表后,可显著减少重定位项的数目(如果是对文本段重定位,每处访问变量的地方都需要一个重定位项,而引入got表后,仅每个目标变量一个重定位项),可减少动态连接器重定位的耗时
缺点
  1. 访问变量时多了一次间接操作(需要先从got项中加载变量的地址),速度稍有影响

看个访问外部库变量的例子

object file 中文本段重定向
1
2
3
4
5
c复制代码extern int value;

int readValue() {
return value;
}

反编译后的指令:

1
2
3
4
5
6
7
yaml复制代码Disassembly of section .text:

0000000000000000 <readValue>:
0: 90000000 adrp x0, 0
4: f9400000 ldr x0, [x0]
8: b9400000 ldr w0, [x0]
c: d65f03c0 ret
  1. 第一条和第二条指令用于加载 value 变量的地址到 x0寄存器中。(备注:因为 aarch64 是定长指令集,每条指令固定4字节,使用这两条指令,可以访问PC +/-4GB的范围)
  2. 第三条指令从x0所表示的地址中读出数据放到w0寄存器中。(备注:int是32位,所以保存到w0,x0的高32位会自动清0)

上面第一条,第二条加载地址的指令是“不完整”的,实际情况应该是:

1
2
css复制代码adrp	x0, pageAddr
ldr x0, [x0, pageOffset]

但是因为外部符号value的地址在其库运行时被加载后才能确定,编译时是无法知晓的,因此编译的时候pageAddr 和 pageOffset都留空(填0),等待重定向,可以看下重定向信息:

1
2
3
4
sql复制代码Relocation section '.rela.text' at offset 0x1e0 contains 2 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000000 0000000b00000137 R_AARCH64_ADR_GOT_PAGE 0000000000000000 value + 0
0000000000000004 0000000b00000138 R_AARCH64_LD64_GOT_LO12_NC 0000000000000000 value + 0

从offset可以看到这两条重定位项分别是针对上面第一条、第二条指令的。
对于文本段(.text section)重定向很直接,也好理解,但是这意味着会改动文本段,那么就不能在多个进程中共享了,内存占用会增加。

动态库GOT表重定向

将上面的 object file(.o)链接(program linker,区别于dynamic linker)一下,反编译看看:

1
2
3
4
5
6
7
yaml复制代码Disassembly of section .text:

0000000000000238 <readValue>:
238: f00000e0 adrp x0, 1f000
23c: f947f000 ldr x0, [x0, #4064]
240: b9400000 ldr w0, [x0]
244: d65f03c0 ret

从前两条指令可以看到是从相对pc 0x1ffe0的位置读取value的地址,来看下重定向信息:

1
2
3
sql复制代码Relocation section '.rela.dyn' at offset 0x220 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
000000000001ffe0 0000000200000401 R_AARCH64_GLOB_DAT 0000000000000000 value + 0

从上面重定位项可以看到0x1ffe0处重定向后正是存入value的地址,同时文本段不需要重定位。从section header table中可以看出0x1ffe0位于.got section。

1
2
3
4
5
6
7
8
9
10
11
less复制代码Section Headers:

[Nr] Name Type Address Off Size ES Flg Lk Inf Al

[ 0] NULL 0000000000000000 000000 000000 00 0 0 0

...

[ 9] .got PROGBITS 000000000001ffd8 00ffd8 000010 08 WA 0 0 8

...

从上面可知.got section是由链接器(program linker)生成的,那么链接器是怎么知道要创建 got 项的呢?回顾一下上面 object file 的重定位信息:

1
2
3
sql复制代码Relocation section '.rela.text' at offset 0x1e0 contains 2 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000000 0000000b00000137 R_AARCH64_ADR_GOT_PAGE 0000000000000000 value + 0

可以看到object file中有一个R_AARCH64_ADR_GOT_PAGE类型的重定位项,正是这个类型的重定位项告知链接器要创建相应的 got 项,以及对应该 got 项的 R_AARCH64_GLOB_DAT 类型的重定位项。

PLT

PLT表简介

PLT(Procedure Linkage Table),用于调用非static函数的。PLT中的代码也会用到GOT表,对应于PLT的GOT表通常会单独一个section:.got.plt

调用非static函数时,会先跳转到对应的PLT项,然后PLT项中的跳板代码会跳转到对应的.got.plt项中的地址,而这个地址是动态链接器填入的目标函数的地址。

延迟绑定

由于库中很多函数运行时可能用不到,比如崩溃处理的函数多数情况下是用不到的,另外像libc中提供了大量的函数,但是app可能只使用其中很少的一部分,因此加载动态库时绑定所有的函数就有点浪费了,因此就引入了延迟绑定:当第一次调用函数时才触发动态链接器去查找目标函数地址。

引入PLT的优缺点

优点
  1. 引入plt表(.got.plt)后,重定向时,文本段不需要修改,因此文本段可以在多个进程中共享,可减少内存使用
  2. 引入plt表(.got.plt)后,可显著减少重定位项(跟上面GOT类似)
  3. 引入plt表后,方便实现延迟绑定
缺点
  1. 调用函数时多了一层间接,性能稍有影响

备注:其实上面提到的优缺点主要都是 .got.plt 带来的,.plt section 本身并不一定要存在,编译时甚至可以通过 -fno-plt 来禁止生成 .plt section。只是将跳板代码(尤其是支持延迟绑定的情况)抽出来放到单独的 .plt section ,指令体积会减小,并且.got.plt中的初始值计算更方便。

看个调用外部库函数的例子

object file文本段重定向
1
2
3
4
5
c复制代码#include<stdlib.h>

void* malloc_proxy(size_t size) {
return malloc(size);
}

反编译后的指令:

1
2
3
4
5
6
7
8
yaml复制代码Disassembly of section .text:

0000000000000000 <malloc_proxy>:
0: a9bf7bfd stp x29, x30, [sp, #-16]!
4: 910003fd mov x29, sp
8: 94000000 bl 0
c: a8c17bfd ldp x29, x30, [sp], #16
10: d65f03c0 ret

bl 的目标地址 0 是因为编译时不知道malloc 函数的地址,需要重定向:

1
2
3
sql复制代码Relocation section '.rela.text' at offset 0x1f8 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000008 0000000b0000011b R_AARCH64_CALL26 0000000000000000 malloc + 0

跟上面一样,在object file中是不存在.got .plt这些section的,重定向是直接针对文本段来的,.plt .got.plt section 是由链接器(program linker)创建的。

动态库PLT GOTPLT表

将object file链接后反编译如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
yaml复制代码Disassembly of section .plt:

0000000000000250 <.plt>:
250: a9bf7bf0 stp x16, x30, [sp, #-16]!
254: f00000f0 adrp x16, 1f000
258: f947fe11 ldr x17, [x16, #4088]
25c: 913fe210 add x16, x16, #0xff8
260: d61f0220 br x17
264: d503201f nop
268: d503201f nop
26c: d503201f nop

0000000000000270 <malloc@plt>:
270: 90000110 adrp x16, 20000
274: f9400211 ldr x17, [x16]
278: 91000210 add x16, x16, #0x0
27c: d61f0220 br x17

Disassembly of section .text:

0000000000000280 <malloc_proxy>:
280: a9bf7bfd stp x29, x30, [sp, #-16]!
284: 910003fd mov x29, sp
288: 97fffffa bl 270
28c: a8c17bfd ldp x29, x30, [sp], #16
290: d65f03c0 ret

从反编译可以看到:

  1. malloc_proxy 改为调用 malloc@plt 的跳板代码(bl 270,偏移 0x270 处的符号:malloc@plt)
  2. malloc@plt 中前两条指令将 PC 偏移 0x20000 处的值读入 x17寄存器中,第4条指令挑转到x17对应的地址处。(先忽略x16)
    来看下重定向信息:
1
2
3
sql复制代码Relocation section '.rela.plt' at offset 0x230 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000020000 0000000100000402 R_AARCH64_JUMP_SLOT 0000000000000000 malloc@GLIBC_2.17 + 0

从重定向信息可以看到,动态链接器会在偏移0x20000处存入 malloc函数的地址。

(备注:malloc 后面的 @GLIBC_2.17 是symbol versioning,本文忽略)

因此 br x17就跳转到了 malloc 函数中,实现了对 malloc 的调用。

(备注:因为 plt 中的跳板代码是通过 br 而不是 blr ,没有修改 lr 寄存器,因此目标函数返回后能回到最初调用位置的下一条指令)

  1. 从 section header table 中可以看到偏移 0x20000 处于 .got.plt section中。
1
2
3
4
5
6
7
8
9
10
11
less复制代码Section Headers:

[Nr] Name Type Address Off Size ES Flg Lk Inf Al

[ 0] NULL 0000000000000000 000000 000000 00 0 0 0

...

[13] .got.plt PROGBITS 000000000001ffe8 00ffe8 000020 08 WA 0 0 8

...

另外我们可以看下 .got.plt section 中,0x20000处的值(动态库中静态的值,动态链接器绑定该符号前):

1
2
makefile复制代码<.got.plt>:
20000: 0000000000000250

从这个地址可以看到:.got.plt 项(除了前3个项)中的初始值对应的是 .plt section中的第一个项:

1
2
3
4
5
6
yaml复制代码0000000000000250 <.plt>:
250: a9bf7bf0 stp x16, x30, [sp, #-16]!
254: f00000f0 adrp x16, 1f000
258: f947fe11 ldr x17, [x16, #4088]
25c: 913fe210 add x16, x16, #0xff8
260: d61f0220 br x17
  • adrp & ldr 是将 PC 偏移 0x1fff8处的值(也就是 .got.plt的第3项)加载到 x17寄存器中
  • .got.plt中的第3项是动态链接器存入的用来查找符号的函数的地址
  • stp 保存 x16 lr 到栈上,因为动态链接器符号查找方法内部会修改lr寄存器,所以先保存到栈上
  • br x17就是跳转到动态链接器符号查找函数去解析目标方法地址,并填入 .got.plt 对应的项中,从栈上恢复寄存器(lr),并跳入该地址执行目标方法

回顾一下上面1,2,3,可知通过plt调用非static方法的流程如下:

  1. 调用 plt 中目标方法对应的跳板方法:xxx@plt
  2. xxx@plt 方法跳转到 .got.plt 对应项中的地址
  3. 如果目标符号已经绑定过 .got.plt 中对应项的地址就是目标函数的地址,调用过程完成
  4. 如果目标符号未绑定过,.got.plt中对应项的地址指向 .plt 中的另一段跳板代码,该代码跳转到动态链接器的符号查找代码,会将查找到的符号地址填入 .got.plt的对应项中,并跳转到目标符号地址以完成方法执行
动态库 BIND_NOW

Android 平台(arm架构)动态库默认是 立即绑定(bind now)的。但Linux平台上默认是延迟绑定的(上面已经提到过延迟绑定的优缺点),对于一个指定的动态库如何判断是否是立即绑定的呢?

  1. 如果动态库 .dynamic section中存在 DT_BIND_NOW 项,那么会立即绑定
  2. 如果动态库 .dynamic section中DT_FLAGSvalue设置了DT_BIND_NOW或者DT_FLAGS_1value设置了DF_1_NOW的话,会立即绑定
  3. 如果运行时环境变量包含LD_BIND_NOW,会立即绑定
  4. 如果调用dlopen时,指定RTLD_NOW的话,会立即绑定
延迟绑定时获取函数地址

上面讲过了对于延迟绑定的情况下,等到第一次调用函数时才会调用动态链接器的方法去查找符号。那么如果在调用函数之前,先要获取函数地址怎么办呢?其实这个很简单,获取函数地址跟变量访问一样,会在.got表中有一个单独的表项存储对应函数的地址,也会有一个单独的重定位项,对于变量地址,动态链接器都是立即绑定的。因此对于同一个函数,在同一个库中可能会同时存在 plt(.got.plt) 项 & got 项。

看一个获取外部函数地址的例子:

1
2
3
4
5
6
c复制代码#include<stdlib.h>
#include<stdio.h>

void f() {
printf("%p\n", malloc);
}

看下他的重定位项:

1
2
3
4
5
sql复制代码Relocation section '.rela.dyn' at offset 0x3b8 contains 8 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
...
000000000001ffd0 0000000500000401 R_AARCH64_GLOB_DAT 0000000000000000 malloc@GLIBC_2.17 + 0
...

全局符号介入

前文提到,访问全局变量、非static函数需要从相应的got项中取出符号的地址,对于外部库的符号这个好理解,对于同一个库中的符号,访问代码与被访问的符号之间的距离是确定的,为什么也要从got中获取地址呢?

原因就是“全局符号介入”的存在:当向全局符号表加入符号时,如果同名符号已存在,则忽略后面的符号。因此访问同一个库中的全局变量或者非static的函数,运行时实际访问的可能是先加载的库中的同名符号。

因此如果某个符号,设计上并不希望外部使用的话,应该将其设置为 static 的,或者放到C++匿名命名空间中,或者将他的 visibility 设置为 hidden。这样还可以优化性能。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…686970…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%