前言

在【关于可变合约的二三事】相关的文章中,我们讨论了代理合约的使用方式,但文中的最后,也提到了,给出的代理合约例子是有问题的,不能用在真实项目中,哪真实项目中是怎么使用代理合约的呢?本文就以最近比较火热的项目TwitterScan作为分析对象,讨论其使用代理合约的方式。

在讨论前,有必要提一下,网上很多代理合约的文章都脱离了真实项目,即你了解完后,依旧看不懂真实项目的合约,比较难受。最好的学习方式还是专业机构的文档+真实在跑的项目,这样才能验证你自己的理解是否正确。

TwitterScan简介

因为有些朋友可能不知道TwitterScan是什么项目,这里简单介绍一下。

TwitterScan官网:https://twitterscan.com/

一个Web3数据分析工具,可以追踪链上数据(即各种Token和链的交易数据)还可以追踪Twitter Web3圈KOL社交发言等链下数据。

嗯,这些都不是重点,重点是,这个项目在短短几个月内,获利536.68个ETH(即获利近500w RMB),而且,技术难度也不会太高。

这里,插一下我目前的一个观点:web3工具类的项目,开发难度不会特别大,但起来的项目收益都很高,所以开发者同行们,可以多多关注一下这个领域的工具

简单代理合约的问题

基于【关于可变合约的二三事】一文,我们知道代理合约会通过delegatecall函数来实现委托调用,其目的是将项目数据放在代理合约中,将项目玩法逻辑放在逻辑合约中,当项目需要更新时,直接替换代理合约关联的逻辑合约,便可以无痛切换成新的玩法且数据不会丢失。当然,逻辑合约出现了bug也可以利用这种方式修改一下。

那代理合约出现bug了呢?抱歉,救不了,要救只能通过社交更新法,即告诉大家,旧的合约有问题,一起来玩新的合约吧,我们官方不承认旧合约里的数据了,嗯,这就是所谓的社交更新,你要让大家配合你玩。

为了稳妥起见,多数项目都不会自己去开发代理合约,而是直接使用专业机构提供的代理合约模型,比如openzeppelin提供了不同模式的代理合约玩法,TwitterScan项目便使用openzeppelin提供的透明代理合约。

在看TwitterScan的合约前,有必要讨论一下简单代理合约的问题。

slot clash

solidity语言有个比较隐晦的特性,那便是slot的分布,很多人看solidity的语法与JavaScript类似,就模仿JavaScript的形式写代码,从而在这里踩坑。

为了方便你理解,这里写给出一个简单的Proxy合约和逻辑合约:

// SPDX-License-Identifier: MITpragma solidity ^0.8.4;contract Proxy {address public implementation; address public admin;uint256 public x = 0;constructor() {admin = msg.sender;}modifier onlyAdmin {require(msg.sender == admin);_;}function setImplementation(address _implementation) external onlyAdmin{implementation = _implementation;}fallback() external payable {_delegate();}receive() external payable {_delegate();}function _delegate() internal {assembly {let _implementation := sload(0)calldatacopy(0, 0, calldatasize())let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)returndatacopy(0, 0, returndatasize())switch resultcase 0 {revert(0, returndatasize())}default {return(0, returndatasize())}}}}contract LogicsContract {// 与代理合约中变量的顺序要一致address public implementation;address public admin;uint256 public x = 0;function add_x(uint256 _x) external returns(uint256) {x += _x;return x;}}

上面的代码是从【关于可变合约的二三事】中直接复制过来的,看到LogicsContract中的注释,solidity要求你,逻辑合约要与代理合约的变量顺序是一直的,究其原因,便是slot。

在solidity中,每个合约中的变量都是按顺序放置的,比如LogicsContract合约中的implementation变量在第0个slot,admin变量则在第1个slot,它刚好与proxy合约中的implementation变量对应上,如果你将顺序调整一下,比如调整成如下形式:

contract LogicsContract {// x => 0 slotuint256 public x = 0;// implementation => 1 slotaddress public implementation;// admin => 2 slotaddress public admin;function add_x(uint256 _x) external returns(uint256) {x += _x;return x;}
}

如果调整成上面的顺序,那么x变量就在第0个slot了,而proxy中,第0个slot是implementation变量,这样当我们调用add_x函数时,在LogicsContract中,处理的是第0个slot,即x变量,但映射到Proxy中,则变成改变的是implementation变量,这种情况便是slot clash(插槽冲突)。

当我在阅读slot clash相关的文档时,有个容易遗漏的细节。

如果光看教程文档,会天真的以为proxy合约中变量的顺序与逻辑合约中变量的顺序一致就好了,但当你去看TwitterScan项目的合约代码时,发现proxy相关的合约中没有相应的变量,以上面的例子为例便是:LogicsContract合约中有x变量,Proxy合约中没有定义x变量,那当我们通过delegatecall委托调用add_x时,Proxy合约将数据存在哪里?都没有定义x变量,不会报错吗?

Proxy合约会将数据存在相应的slot位置中,比如LogicsContract合约中x变量在第0个slot,就算Proxy合约中没有定义x变量,当我们委托调用,操作x变量时,也可以正常操作,其中第0个slot便是其x变量,只是没有显示的声明出来。

我画个图简单总结一下:

当我们通过delegatecall调用chnage_a函数时,change_a函数会去修改LogicsContract合约中a变量,而a变量在第0个slot,因为delegatecall函数调用后,数据会在ProxyContract合约中保存,所以change_a函数的效果便是覆盖了ProxyContract合约的owner变量,如果owner变量记录着发布当前ProxyContract合约的用户address,而转账函数又限制成只有owner可以调用,那么此时ProxyContract合约就变成了黑洞合约(无法将里面的Token转出)。

当我们通过delegatecall调用change_c函数时,change_c函数会去修改LogicsContract合约中c变量,c变量的slot为2,ProxyContract合约中虽然不存在c变量,但依旧可以将值存下来。

怎么验证上面我说的是对的?当然是做实验!

我们按上图的形式,写下如下代码:

// SPDX-License-Identifier: MITpragma solidity ^0.8.4;contract ProxyContract{address public owner;constructor(address _owner) {owner = _owner;}function delegatecallChangeA(address LogicsContractAddr, uint256 _a) public returns(uint256) {(, bytes memory data)  = LogicsContractAddr.delegatecall(abi.encodeWithSignature("change_a(uint256)", _a)); return abi.decode(data, (uint));}function delegatecallChangeC(address LogicsContractAddr, uint256 _c) public returns(uint256) {(, bytes memory data)  = LogicsContractAddr.delegatecall(abi.encodeWithSignature("change_c(uint256)", _c)); return abi.decode(data, (uint));}
}contract LogicsContract {uint256 public a;uint256 public b;uint256 public c;function change_a(uint256 _a) public returns(uint256) {a = _a;return a;}function change_c(uint256 _c) public returns(uint256) {c += _c;return c;}
}

然后将LogicsContract合约与ProxyContract合约都部署上。

ProxyContract合约中有owner变量,用于记录部署当前合约的账户地址,如下图所示:

当我们调用delegatecallChangeA函数后,owner变量就变成了6,这是因为ProxyContract合约的owner变量与LogicsContract合约的a变量在同一slot位置,所以改变a变量其实会影响到代理合约的owner变量,从而导致get_name函数再也无法被任何人调用,如果get_name函数是其他转账相关的函数,这个合约就不可用了。

随后,我们调用两次delegatecallChangeC函数时,发现decode ouput会是12,即Proxy中,位置为2的slot其实存了c变量的值。

聊了这么多slot clash会出现的问题,那有什么解决方法呢?

有,那便是EIP1967提出的解决方案,为Proxy合约中一些必要的变量指定slot的位置,因为是我们自定义的slot位置,某些变量不再按从0到N的slot顺序排布,openzeppelin将这种slot存储模式的代理称为:非结构化存储代理(Unstrctured Storage Proxies)

我们假设在Proxy合约中,定义Implementation变量来存储逻辑合约的address,定义admin变量来存储部署Proxy合约的账户地址,为了避免slot clash,EIP1967会根据如下方式来定义这两个变量的slot:

// 计算Implementation变量的slot位置
// IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1
));// 将Implementation变量的slot位置指定成计算出的IMPLEMENTATION_SLOT
function _implementation() virtual override internal view returns (address impl) {bytes32 slot = IMPLEMENTATION_SLOT;assembly {impl := sload(slot)}
}// admin变量也一样
// ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
bytes32 private constant ADMIN_SLOT = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1
));function _admin() internal view returns (address adm) {bytes32 slot = ADMIN_SLOT;assembly {adm := sload(slot)}
}

自定义的slot位置被LogicsContract合约中变量覆盖的概率微乎其微。

keccak256('eip1967.proxy.variable_name')) - 1 这种方式是EIP1967约定俗成的方式,你当然可以自定义成其他的slot位置,让其概率足够小则可,但如无必要,按约定走吧。

关于EIP 1967的更多细节,可以阅读其文档:https://eips.ethereum.org/EIPS/eip-1967

这里有个误区需要注意,EIP 1967主要解决的是Proxy合约中变量slot clash的问题,但无法解决逻辑合约冲突的问题,如下:

|Implementation_v0   |Implementation_v1        |
|--------------------|-------------------------|
|address _owner      |address _lastContributor | <=== Storage collision!
|mapping _balances   |address _owner           |
|uint256 _supply     |mapping _balances        |
|...                 |uint256 _supply          |
|                    |...                      |

上面这种形式是Implementation_v0逻辑合约升级成Implementation_v1逻辑合约,但原本slot为0位置的变量被新的_lastContributor变量覆盖,这就会出问题了,所以在升级逻辑合约时,如果要添加新的变量,按顺序添加在后面,如下:

|Implementation_v0   |Implementation_v1        |
|--------------------|-------------------------|
|address _owner      |address _owner           |
|mapping _balances   |mapping _balances        |
|uint256 _supply     |uint256 _supply          |
|...                 |address _lastContributor | <=== Storage extension.
|                    |...                      |

function clash

除了slot clash外,代理合约模式还可能会遇到function clash。我们知道,solidity中有函数选择器的概念,当用户调用某个函数时,函数选择器会选择出用户要调用的函数。

函数选择器在匹配函数时,使用函数签名hash后的前4个字节,而不是通过函数名,这就会出现两个函数名完全不同的函数会被同时匹配上,如下图:

如果你在同一个合约中,solidity会检测出function slot,但代理合约模式下,函数会分布到多个合约,此时就无法检测function clash了。

**假设Proxy合约与Logics合约上的函数出现了function clash,就会出现调用该函数时,没有调用到Logics合约上,而使用了Proxy合约中相应的函数,导致出现逻辑bug。**区块链世界中其实出现过相关的安全问题,详情可看:https://medium.com/nomic-foundation-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357

为了解决这个问题,openzeppelin给出了透明代理合约的模式(Transparent Proxy),这种模式很好理解,在Proxy合约中定义一个admin变量,用于记录可以操作Proxy合约中函数的地址,后续用户使用Proxy合约时,不论是否出现functino clash,只要调用方的address与admin变量记录的一致,则只能调用Proxy合约中的函数,不通过delegatecall进行委托调用,若Proxy合约中不存在相关的函数,则直接报错。如果调用方的address与admin变量记录的不一致,则所有的调用都会被委托调用到逻辑合约中,对于调用方而言,不需要考虑function clash的情况。

具体的实现方式很简单,定义一个modifier判断当前调用者是否为admin则可,例子如下:

modifier ifAdmin() {if (msg.sender == _admin()) {// 如果admin,则直接调用Proxy合约中的函数_;} else {// 如果不是admin,则触发委托调用的逻辑_fallback();}
}// 代理合约中用于更新逻辑合约的方法,只有admin address才能调用
function upgradeTo(address newImplementation) external ifAdmin {_upgradeTo(newImplementation);
}function _fallback() internal {// 如果admin调用的函数,在proxy不存在时,会触发fallback// 在fallback中,通过_willFallback函数判断当前调用者是否为admin,如果是,则返回报错,// 不再执行委托调用的逻辑_willFallback();_delegate(_implementation());
}function _willFallback() virtual override internal {require(msg.sender != _admin(), "Cannot call fallback function from the proxy admin");
}

但这也存在一个问题,如果admin中记录着当前部署Proxy合约的用户address,那么这个用户在使用时,便只能使用代理合约中的函数,即部署Proxy合约的用户无法成为一个正常账户。

举一个具体的例子,假设Proxy合约中有owner()和upgradeTo()函数,逻辑合约中有owner()和Transfer()函数,当调用者是owner(Proxy合约部署者)和other address(其他普通用户)时,会有如下情况:

msg.sender owner() upgradeto() transfer()
Owner returns proxy.owner() returns proxy.upgradeTo() fails
Other returns erc20.owner() fails returns erc20.transfer()

因为Owner代理合约部署者,所以无法成功调用逻辑合约中的transfe()r函数,而Other是普通用户,无法成功调用代理合约中的upgradeto()函数,Other账户是正常现象,但Owner用户连转账都不能操作,不能作为正常用户来使用了,你可以将Owner账户就专门用于管理Proxy合约,再创建其他账户进行正常交互,嗯,就麻烦了些。

为了解决这个问题,openzeppelin在透明代理合约的模式中添加了ProxyAdmin合约,这个合约负责管理代理合约的所有函数,比如修改代理合约关联的逻辑合约,从而实现玩法更新等,大体关系图如下:

  • 1.Admin User创建了ProxyAdmin合约、Proxy合约和Logics合约,是项目方的账户。

  • 2.Proxy合约的admin变量设置成ProxyAdmin合约,后续Proxy合约中的函数只允许ProxyAdmin合约调用。

  • 3.Other User是普通用户,他们可以正常使用Proxy合约,Admin User地址因为与admin变量中记录的地址不同,所以也可以正常使用Proxy合约。

这便是openzeppelin提供的透明代理合约的实现方式,TwitterScan项目便使用了这种模型。

构造函数不被调用

当我们在看真实项目时,还会发现一个现象,被代理的逻辑合约中,通常没有构造函数,为何?

这是因为,代理合约无法使用逻辑合约的构造函数。如果逻辑合约中有构造函数,在逻辑合约部署时,构造函数会自动调用,数据会留存在逻辑合约中,此时再与代理合约关联上时,逻辑合约中构建函数的逻辑与数据没有任何意义。

但我们很多业务都需要构造函数来实现,比如初始化NFT的名称、数量等。

openzeppelin提供了Initializable合约来解决这个问题,这个合约中,提供名为Initializiable的modifier,这个modifier可以确保函数只能被调用一次,其实现如下:

uint8 private _initialized;bool private _initializing;modifier initializer() {bool isTopLevelCall = !_initializing;// 被调用过的函数,如果通过该requirerequire((isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1),"Initializable: contract is already initialized");_initialized = 1;if (isTopLevelCall) {// 被调用后,记录一下_initializing = true;}_;if (isTopLevelCall) {_initializing = false;emit Initialized(1);}
}

阅读initializer的源码,可以发现,initializer实现确保某函数单次执行的效果其实就是基于一个变量来记录函数是否被调用过,仅此而已。

通过initializer,我们将原本是需要通过构造函数中逻辑放在某个函数中,当代理合约初始化时,调用这个函数,因为有initializer的存在,可以确保改函数只被调用一次,从而实现逻辑合约中构造函数的效果。

TwitterScan合约代码阅读

前面我们了解了相应的基础后,便可以阅读TwitterScan项目的合约代码了。

其代理合约的地址为:https://etherscan.io/address/0xd9372167ef419cfbbcd6483603ad15976364e557#writeProxyContract

我个人习惯使用deth.net来阅读合约代码,使用方式如下:

deth.net工具会帮你打开当前的合约代码。如果当前合约是Proxy合约,它还会帮你找到逻辑合约,然后一同展示出来,效果如下:

其中__AdminUpgradeabilityProxy__是代理合约,TwitterscanPass是逻辑合约,代理合约记录数据,逻辑合约实现项目的玩法。

本文主要关注Twitterscan项目使用代理合约的形式,所以不去过多关注其玩法代码。

我个人习惯是,通过deth.net简单浏览合约的代码,但真正要深入理解时,光静态分析是不够的,我还会将合约代码扒下来,在remix上运行起来,看具体的效果,来验证自己的理解是否正确。

怎么扒呢?因为项目引用了openzeppelin中较多的代码,一个个复制粘贴,不太优雅,我个人会用:https://smart-contract-downloader.vercel.app/工具来下载。

下面我们来看其具体的实现逻辑。

首先,看到__AdminUpgradeabilityProxy__合约,它继承了2个合约,并在构造方法中,调用了UpgradeabilityProxy合约的构造函数。

其实这些合约都会继承基本的Proxy合约。

基本的Proxy合约就是最简单的代理合约,通过_delegate函数与fallback函数来实现代理功能。

BaseUpgradeabilityProxy合约继承了Proxy,然后基于EIP1967的方式,指定了代理合约中变量的slot位置,避免slot clash。

此外,BaseUpgradeabilityProxy合约还实现了ProxyAdmin的功能,对于代理合约,只有Admin才能调用。

然后我们看回UpgradeabilityProxy合约的构造函数,__AdminUpgradeabilityProxy__合约的构造函数中调用了UpgradeabilityProxy合约的构造函数,其代码如下

我们看到逻辑合约,即TwitterscanPass.sol代码,看到它的initialize函数,该函数只能被调用一次。

很明显,UpgradeabilityProxy合约的构造函数会调用逻辑合约TwitterscanPass中的initialize函数,实现数据的初始化。

然后因为slot的原因,代理合约中,关键的变量,都采用了自定义slot的形式,逻辑合约中的这些变量会正常的映射到代理。

为了验证上面的分析,我们在TwitterscanPass逻辑合约中,加上ayu_number这个uint256类型的变量,然后定义一个函数来获取这个值,代码如下:

然后我们部署一下__AdminUpgradeabilityProxy__合约与TwitterscanPass合约。

因为TwitterscanPass合约中的逻辑比较长,编译时会出现下面问题:

此时我们开启Remix IDE的编译优化,便可以解决这个问题:

因为部署__AdminUpgradeabilityProxy__合约时,需要逻辑合约address、Admin address和调用TwitterscanPass合约initialize函数的encode编码。

为了方便,Admin address我直接使用了当前的账户address,而encode编码,通过下面代码,可以获得。

contract GetInitFunc{function get_func_hash() public pure returns(bytes memory) {// 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 为 部署 __AdminUpgradeabilityProxy__ 合约的addressreturn abi.encodeWithSignature("initialize(address)", 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4);}
}

部署__AdminUpgradeabilityProxy__合约

部署后,如果理解成功,TwitterscanPass合约的initialize函数会被调用,而initialize函数中设置的ayu_number=666便会生效。

为了判断ayu_number是否生效了,我写了如下代码:

// SPDX-License-Identifier: MITpragma solidity ^0.8.4;contract Caller {address public proxy;constructor(address _proxy) {proxy = _proxy;}function get_ayuname_proxy() external returns(uint256) {// 调用 get_ayu_number函数,查询 __AdminUpgradeabilityProxy__ 合约中的ayu_number变量(, bytes memory data) = proxy.call(abi.encodeWithSignature("get_ayu_number(uint256)", 1));return abi.decode(data, (uint256));}
}

部署上面的合约,将__AdminUpgradeabilityProxy__合约地址设置为上述合约的proxy变量,然后委托调用get_ayu_number函数,因为数据是存在__AdminUpgradeabilityProxy__合约中,所以会获得ayu_number的值,效果如下:

结合Twitterscan项目的合约代码和实验结果,说明我们对透明代理的理解到位了,真实项目中,确实是如我们理解那般使用的。

结尾

区块链世界很有趣,我目前在开发NFT相关的工具,欢迎这个圈子的朋友找我玩。

我是二两,我们下篇文章见。

从TwitterScan项目看代理合约的使用相关推荐

  1. 从天气项目看Spring Cloud微服务治理

    网上搜集的资源,个人感觉还行,分享了 从天气项目看Spring Cloud微服务治理 网盘地址:https://pan.baidu.com/s/1ggn5uld 密码: n6bn 备用地址(腾讯微云) ...

  2. 从Folding@home项目看GPU通用计算发展

    GPU通用计算发展历程简析 前言:Folding@home(蛋白质折叠过程研究)项目GPU客户端的推广,使得普通玩家也有机会体会GPU通用运计算能力,让显卡也能成为支持公益事业的重要力量.分布式计算的 ...

  3. v54.04 鸿蒙内核源码分析(静态链接) | 一个小项目看中间过程 | 百篇博客分析HarmonyOS源码

    子曰:"回也其庶乎,屡空.赐不受命,而货殖焉,亿则屡中." <论语>:先进篇 百篇博客系列篇.本篇为: v54.xx 鸿蒙内核源码分析(静态链接篇) | 一个小项目看中 ...

  4. 怎么让前端项目运行起来_如何立即使您的前端项目看起来更好

    怎么让前端项目运行起来 We've all been there. You've been learning the basics of front end Web development, and ...

  5. 想自己创业当老板?找不到好的项目?看完这篇文章就知道做什么了!

    首先我告诉大家那些说什么,每天什么都不用多干,动动手指,按几下页面就能日入斗金的纯粹是在说梦话,你要是还真信了你就是当代韭菜.是的我也是悔不当初,在最开始不了解这一行的时候,我以为它和淘宝刷单一样简单 ...

  6. 这些Python项目看上去不错的样子

    目前是35个Python项目,会继续保持更新.Learn by doing才是正确的技术学习姿势. 20160918更新: Python - Python3 实现火车票查询工具 20160816更新: ...

  7. 这个开源项目...看了就停不下来啊!

    今天是周末,周末就应该放松放松,小编就给大家带来一个娱乐性较高的项目,中国表情包大集合! 说起表情包,大家能想到什么?是黑人笑脸? 是闪电五连鞭? 还是各种鬼畜表情呢?此处考虑到某些粉丝情绪就不放图了 ...

  8. 谷歌、微软、OpenAI等巨头七大机器学习开源项目 看这篇就够了

    在人工智能行业,2015-2016 出现了一个不同寻常的趋势:许多重量级机器学习项目纷纷走向开源,与全世界的开发者共享.加入这开源大潮的,不仅有学界师生,更有国内外的互联网巨头们:国内有百度和腾讯,国 ...

  9. 用户之声 | 从江苏新大陆项目看GBase国产数据库

    文 | 田一枫 一直从事数据库行业的我从未想过,有一天,数据库的国产化浪潮会到来的如此迅速.坚决和势不可挡.这是我参与了南大通用GBase 8a认证华西专场训练营的最大感受.曾几何时,我对国产数据库的 ...

最新文章

  1. 设置编码格式为utf8
  2. qgis经纬度_数据养成系列--QGIS地理空间
  3. 众核多计算模式系统的构建 - 全文
  4. LeetCode算法
  5. 利用shell和iptables实现自动拒绝恶意试探连接SSH服务
  6. 详解DMZ的部署与配置:ISA2006系列之二十九
  7. TCP校验值的伪头以及校验值计算
  8. Linux 上的数据可视化工具
  9. Apollo(阿波罗)是携程框架部门研发的分布式配置中心,ubuntu本机安装
  10. LeetCode:每日一题(2020.4.9)
  11. asp:dropdownlist如何去掉三角箭头_如何使用css伪元素实现超实用的图标库(附源码)...
  12. 用过那些号称媲美迅雷的下载神器,发现没一个能打的。
  13. 微信小程序服务器布置轮播图,微信小程序自定义轮播图
  14. 201671010426 孙锦喆 实验二词频统计软件项目报告
  15. 在你的ipad上使用Vscode撸代码(快速操作向)
  16. 如何用3dmax画OpenGL的5大坐标系
  17. windows7 x64x86专业纯净版(usb3.0_nvme)2019.12.17
  18. CF1367D 构造
  19. 从 Azure Databricks 访问 Azure Blob 存储
  20. 基于c语言256色转16色,在16色模式下显示256色及全彩色

热门文章

  1. Win10 hyper-v manager导入虚拟机找不到虚拟机 可能没有权限访问
  2. 使用three.js开发3d地图初探
  3. Ubuntu(Linux)增加新用户并赋予权限、删除用户
  4. 大学计算机上机实验指导与测试pdf,大学计算机基础上机指导与测试-王瑞祥主编.pdf...
  5. Latex 分式怎么打
  6. 微型计算机的主要因素,微型计算机的性能主要取决于( )。
  7. 环信android手机推送苹果收不到消息,环信iOS端离线推送收不到怎么办?(客服)...
  8. Emacs 学习之旅
  9. 关于计算机老师的话,感谢大学老师的话语
  10. 【李开复】给中国学生的第六封信——选择的智慧(六)