首页
我的邻居
留下你的脚印
Search
1
用PDO得知数据库中某个表的总行数
11,297 阅读
2
积木报表大屏配置动态数据时php api跨域问题的解决
152 阅读
3
layui实现表格分页和总数
149 阅读
4
部署 Memos 接入 QQ 机器人
135 阅读
5
layui弹出提示框并关闭子页刷新父页面
132 阅读
默认分类
PHP
登录
Search
标签搜索
layui
php
api
layuimini
pdo
mysql
积木报表
php跨域
php表格分页
layui表格分页
Moxne
累计撰写
13
篇文章
累计收到
2
条评论
首页
栏目
默认分类
PHP
页面
我的邻居
留下你的脚印
搜索到
6
篇与
的结果
2023-05-13
配置一个舒适的 zsh 环境
作为 *nix(指 macOS)重度用户,也作为 VPS 玩家,每天接触最多的就是终端。一个高效、美观的终端可以显著提升日常作业的效率和幸福感。安装使用 zsh一般的 Linux 发行版,默认 Shell 大多是 bash,可能需要自己安装 zsh。macOS 的默认 Shell 就是 zsh。通过 echo $SHELL 可以查看当前使用的 Shell。通过 cat /etc/shells 可以查看系统安装的所有 Shell。安装后,在终端输入 zsh 就可以进入 zsh。运行 chsh -s /bin/zsh 可以将默认 Shell 换成 zsh,这样打开终端或者用 SSH 登录,默认就会使用 zsh。如果没有进行配置,刚打开 zsh,可能感觉和 bash 的区别并不大。事实上,zsh 最强大之处就在于其可拓展性以及插件生态。配置下面这些主题 / 插件,使用体验就可以吊打 bash 了!(我并不喜欢 oh-my-zsh,原因之一是启动有点慢。这里我还是用原生 zsh 配置了)Powerlevel10kPowerlevel10k 是一套 zsh 主题,强调速度、灵活性以及开箱即用的体验。Github 项目地址这个主题非常强大,第一次使用就惊艳到了我。Powerlevel10k 初始配置首先第一次使用就有一个非常友好的 configuration wizard 引导你进行「傻瓜式」的配置。这个主题也提供了相当多的样式,可以根据偏好自己选择。不管是简洁还是炫酷的风格都有……而且它还支持集成 git。当进入一个 git 仓库,当前输入行前就会显示对应的信息,包括有多少更改未提交、分支名等等。Powerlevel10k 会显示 git 仓库的信息p10k 本身提供了丰富的插件(这好像是插件的插件)。一切都是可以自定义的。如果不满足于引导式配置程序简单的配置,也可以自己修改相应的配置文件(如图中右边显示的 127.0.0.1:10800 就是我配置了终端显示当前代理)。支持在终端显示时间、网络甚至内存等等各种信息。这个主题的安装也非常方便(其实本质上是 zsh 插件),先 clone 这个 git 仓库(我自己一般都 clone 到 ~/.zsh 目录下),然后在 .zshrc 中引入即可:source ~/.zsh/powerlevel10k/powerlevel10k.zsh-theme重启终端,应该就能看到 configuration wizard。建议配套使用它提供的字体:MesloLGS。这是一个特制的等宽字体,内置了各种小图标,可以显示在终端里。如果不使用这个字体,许多图标就无法显示,当然 wizard 会询问你图标显示是否正常,若回答不正常,配置后就不会显示图标。zsh 插件推荐zsh-autosuggestionszsh 上像 fish shell(另一种功能丰富的 Shell)一样快速且低调的自动建议插件。Github 项目地址这个插件可以根据历史记录等对当前的输入提供建议,建议会以浅灰色的背景显示于当前输入文本「下方」:zsh-autosuggestions 提供了 echo 的建议像这张图中一样,只要输入 ec 两个字符,这个插件就会建议 echo $SHELL(由于我刚刚输入过这个命令),并以灰色显示。我可以照着输入,也可以按方向右键直接自动补全。(是不是有点 Copilot 的感觉 🙃)安装方法类似,clone 仓库后在 .zshrc 里加入:source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh重启终端即可生效。(官方的安装指南也介绍了用包管理器安装的方法,可以参考)zsh-autocompletezsh-autocomplete 为 zsh 终端添加了实时预输入自动补全功能。Github 项目地址这是我最喜欢的一个插件!之前使用过 Fig 这个软件,这是一个终端的拓展程序,可以在输入的时候实时提供一个小下拉列表,显示自动补全的各个候选项。然而劝退我的是这个软件需要登录才能使用,而且随着商业化也变得愈发臃肿。于是我找到了更优秀、更原生的平替,就是 zsh-autocomplete。这个插件和 Fig 一样可以在输入的时候实时提供自动补全,而且是在终端内实时更新的。这被称为「实时预输入自动补全」(real-time type-ahead autocompletion)。即每多输入一个字符,补全列表就会发生变化,根据用户的输入实时更新。更舒服的是它可以实时显示常用命令各种选项的解释文本,有了这个插件基本可以不需要 man 了,也不再需要死记硬背各种 arguments 是什么意思。这个功能大大提高了终端的操作效率。zsh-autocomplete 可以实时显示各种选项的解释和其他插件的用法一样,clone 到本地之后只要在 .zshrc 里 source 即可:source ~/.zsh/zsh-autocomplete/zsh-autocomplete.plugin.zsh这个插件对实时响应的要求比较高,所以如果是网速很慢的国外 VPS(输入一个字母都要等一下的那种)不建议使用,不然可能会非常卡。zsh-syntax-highlightingzsh 上像 fish-shell 一样的语法高亮。Github 项目地址安装这个插件后,终端中输入的合法的命令会显示为绿色,不合法的命令会显示为红色,不用回车运行即可检查输入是否正确。并且输入存在的文件地址时,其会自动显示下划线,也可以一目了然地检查文件地址是否输入正确。大大提升了效率。安装也是 clone 后 source 即可。source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zshGit 同步 zsh 配置最后介绍一个奇技淫巧,用 Git 同步 $HOME(即 ~ 表示的用户目录)下的配置文件。由于我有不止一台 VPS,还有 macOS,平时折腾还经常装新的系统,每次都重新按如上配置 zsh 非常麻烦,所以我用一个 git 仓库存储了 $HOME 目录下的配置文件,包括 zsh、vim 等的配置。具体是这些文件:.vim/.zsh/.gitignore.p10k.zsh.tmux.conf.vimrc.zshrc直接在 $HOME 目录下 git init 就可以初始化空 git 仓库。然而 $HOME 文件夹下有太多繁杂的文件,如何实现只跟踪这几个文件呢?秘诀就在于这个 .gitignore 文件:*没错,要求忽略所有文件。需要添加某个文件或文件夹的时候,可以使用 --force 强制跟踪。git add .zshrc --force这个地方的要求本质上是要用 .gitignore 维护一个白名单(而不是通常的黑名单),我搜集了不少其他解决方案,但是都有不完美之处。忽略全部文件再用 --force 添加,是我认为最简洁优雅的方案了。现在添加 remote,就可以保持同步啦~
2023年05月13日
47 阅读
0 评论
0 点赞
2023-05-13
Typecho 主题 PJAX 无刷新以及遇到的一些问题
最近给 Typecho 主题 Daydream 加上了 PJAX 无刷新。本以为加一段代码就好,结果遇到了一大堆问题,无法提交评论、插件无效、数学公式没法渲染……特此记录一下。什么是 PJAXpjax = pushState + ajax对于传统的 Typecho 主题,实际上就是服务端渲染(SSR)的代表,用户每请求一个页面,服务器(php)就渲染好这个页面的内容(包括文章、评论之类的动态内容),渲染好一个静态的 HTML 然后传给浏览器。这样虽然方便,但是不难发现传输数据量其实不小。在加载不同的页面的时候,有不少部分是重复渲染的,比如页面的页眉、页脚,每个页面都是一样的,但是加载每个页面都要重新加载一次。这就会降低访问体验。而以调用 Restful 接口等方式进行客户端渲染(CSR)的单页应用程序(SPA)则是与之对立的另一个典型。用户第一次访问只加载一个页面框架,其中的 js 代码通过接口向服务器请求数据,然后在客户端进行渲染。进入新的页面链接时只刷新需要更新的部分(一般是通过 DOM 操作)。这样每次只需要修改页面中很少的信息量,可以加快加载的速度。这称为无刷新技术。然而 Typecho 并不是以这种 CSR 的思路构建的。(我之前开过的一个坑 Vuecho 其实就是这种尝试,这是一个 demo:alpha.skywt.cn)那么 PJAX 实际上就是以上两者的「折中方案」。通过这段神奇的 js 代码,它可以自己判断哪些部分刷新了,然后在刷新(或者进入新的页面)时通过 DOM 操作更新要更新的部分。虽然后端的实现完全是服务器端渲染,但是前端看起来就好像是客户端渲染一样,实现全站无刷新,可以获得非常迅捷的体验。给 Typecho 主题加上 PJAX⚠️ 本文使用的是 jquery-pjax。这一步其实非常简单(麻烦的在后面),首先把博客的动态内容(即从一个页面进入另一个页面要更新的内容)用一个 之类的标签包裹起来,并将 id 设置为 pjax-container。这个容器就叫做 PJAX 容器。在这个容器外部,是刷新页面不需要更新的部分,如每个页面都一样的页眉、导航栏、页脚。在这个容器内部,是刷新页面需要重新加载的部分,比如从文章列表进入一篇文章,容器中的内容由「文章列表」改变为「文章内容」。一般主题的结构包含 header.php、footer.php,只要在 header.php 的末尾加上容器的开放 tag,在 footer.php 的开头加上容器的封闭 tag 即可。最后渲染出来的页面应该像是这样:<head> <!-- ... --> </head> <body> <header><!-- 页眉,标题什么的 --></header> <nav><!-- 导航栏什么的 --></nav> <main id="pjax-container"> <!-- 网站主体内容,文章列表 / 文章内容 / 评论什么的 --> </main> <script> // 下面要加上的代码 </script> <footer><!-- 页脚什么的 --></footer> </body>在 footer.php 或者其他地方(PJAX 容器的后面,就是上面这段代码的 部分)加上这段代码:$(document).pjax('a[href^="<?php $this->options->siteUrl()?>"]:not(a[target="_blank"])', {container: '#pjax-container', fragment: '#pjax-container'});$(document).on('pjax:send',function() {// alert('开始加载'); // 开始加载时要运行的代码(如显示加载动画)});$(document).on('pjax:complete', function() {// alert('加载完成'); // 加载完成后要运行的代码(如去除加载动画)});传入 pjax 函数的第一个参数是 selector,它告诉 PJAX,只对于目标为本站内的且没有设置在新页面打开(target="_blank")的链接进行无刷新加载,其他链接(如站外链接)则会正常加载。第二个参数是 container,指定 id 为 pjax-container 的元素作为 PJAX 容器,当然也可以任意修改。可以查阅文档修改更多参数。on 和 complete 都是 PJAX 事件,文档里也列举了更多事件。可以用这个测试一下 PJAX 开启成功了没有。经过以上步骤(应该)可以发现页面之间的切换明显变快了。解决评论问题启用 PJAX 后,如果从一个页面进入另一个页面(比如从首页进入某个文章页面),提交评论会发现页面刷新了一下(或者 Safari 干脆显示无法加载),没法正常发表评论。必须手动刷新这个页面才能发评论。这是因为 Typecho 提交评论的 js 脚本放在 里(F12 可以看到),而当我们刷新页面, 中的脚本并不会更新(因为其在 PJAX 容器之外)。所以只有第一次进入页面可以正常提交评论,进入其他页面后就不行。解决方法也很简单。在评论区加上这段从 里 copy 出来的代码:(function() {window.TypechoComment = { dom: function(id) { return document.getElementById(id); }, create: function(tag, attr) { var el = document.createElement(tag); for (var key in attr) { el.setAttribute(key, attr[key]); } return el; }, reply: function(cid, coid) { var comment = this.dom(cid), parent = comment.parentNode, response = this.dom('<?php echo $this->respondId; ?>'), input = this.dom('comment-parent'), form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0], textarea = response.getElementsByTagName('textarea')[0]; if (null == input) { input = this.create('input', { 'type': 'hidden', 'name': 'parent', 'id': 'comment-parent' }); form.appendChild(input); } input.setAttribute('value', coid); if (null == this.dom('comment-form-place-holder')) { var holder = this.create('div', { 'id': 'comment-form-place-holder' }); response.parentNode.insertBefore(holder, response); } comment.appendChild(response); this.dom('cancel-comment-reply-link').style.display = ''; if (null != textarea && 'text' == textarea.name) { textarea.focus(); } return false; }, cancelReply: function() { var response = this.dom('<?php echo $this->respondId; ?>'), holder = this.dom('comment-form-place-holder'), input = this.dom('comment-parent'); if (null != input) { input.parentNode.removeChild(input); } if (null == holder) { return true; } this.dom('cancel-comment-reply-link').style.display = 'none'; holder.parentNode.insertBefore(response, holder); return false; } };})();注意其中的 <?php echo $this->respondId; ?>,这就是前文评论失败的原因——每个页面的 respondId 不一样,所以这个是需要刷新的。除此之外,需要关闭 Typecho 后台的「设置」-「评论」-「评论提交」中取消勾选「开启反垃圾保护」。这个选项实际上是开启反 CSRF 攻击的防护,即每次加载页面服务器会传一个每次不同的随机字符串(在页面里表单最后面一个名为 _ 的 hidden input 元素),提交评论时表单里会带上它,而用 PJAX 的方式去拿这个字符串非常麻烦,索性关闭了。最好加上一些反垃圾评论的插件,因为没有了反 CSRF 防护,可以用脚本轻易多次提交评论。经过以上的修改,应该可以正常提交评论了。解决插件问题KaTeX 数学公式没法渲染(第一次加载能渲染,进入新页面没法渲染),也是和评论类似的问题。渲染 KaTeX 公式的这段 js 代码:renderMathInElement(document.body, { delimiters: [ {left: "$$", right: "$$", display: true}, {left: "$", right: "$", display: false}, ] });放在 PJAX 容器之外,所以只会在最初加载页面的时候执行一次。它涉及到 DOM 操作,我们希望它每次都能执行。解决方法是放在 PJAX 容器内部,或者在 complete 事件中也执行一次。除此之外,包括 aplayer 插件、fancybox 图片灯箱、代码高亮等等都是类似的问题,只要把 DOM 操作的代码放到 PJAX 容器内部即可。经过以上的操作,基本上网站的 PJAX 就没问题啦。享受极速的响应吧~
2023年05月13日
20 阅读
0 评论
0 点赞
2023-05-13
使用 Wireshark 进行嗅探实验
实验环境手机(iPhone)开启热点作为 gateway,让电脑(MacBook)连接热点作为攻击者,平板(iPad)也连接热点作为 victim。让我们来看看,当 victim 上网时,攻击者能看到什么。混杂模式大多数的网卡都支持开启混杂模式(promiscuous mode),只是一般工作在非混杂模式下。在混杂模式下,网卡可以接收所有经过它的数据流,不论目的地是否是它。在一般 Linux 发行版中需要手动开启混杂模式:ip link set eth0 promisc on开启之后,网卡应该就可以支持混杂模式了。在终端使用 ifconfig 查看网卡接口信息,接口的 flags 中包含 PROMISC 就说明目前支持混杂模式,例如:...en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500options=6463<RXCSUM,TXCSUM,TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM> ether 1c:91:80:e2:0f:f6 inet6 fe80::1878:5529:853f:6fa1%en0 prefixlen 64 secured scopeid 0xc inet 192.168.86.20 netmask 0xffffff00 broadcast 192.168.86.255 inet6 240e:468:490:e659:402:bca2:58e4:dbae prefixlen 64 autoconf secured inet6 240e:468:490:e659:e445:33c7:a581:43c8 prefixlen 64 autoconf temporary nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active...MacBook 的 macOS 中网络接口命名和一般的设备不同,使用 ifconfig 显示时,en0 才是 Wi-Fi 网卡接口。可以在「系统信息.app」查看,或者直接看当前 active 的接口。安装配置 WiresharkmacOS 下使用 homebrew 安装 Wireshark 时,formula 的版本和 cask 的版本存在 conflict,不能共存。只要安装 cask 的版本(带图形界面的)即可。(从官网下载安装是一样的)brew install --cask wireshark开启后会提示安装 ChmodBPF,否则无法使用。安装之后打开会发现还是提示没有安装,这时候还要给权限才能用:sudo chmod 666 /dev/bpf*给权限之后打开 Wireshark 就可以抓包了。注意 macOS 重启之后这个权限会重置,需要重新设置。ARP 欺骗开启了混杂模式只是保障了我们可以嗅探,但是这时候局域网内的其他主机并不愿意把包发给我们。我们要进行 ARP 欺骗,将自己伪装成一个 gateway,让其他主机乖乖地把包发给我们。我们使用 ettercap 工具进行 ARP 欺骗。Windows 平台建议使用 arpspoof,这个程序更加简单轻量。brew install ettercapsudo ettercap -G # 以图形界面方式运行 Ettercap,注意要 sudo!启动 Ettercap 后,进行 host 扫描或者等待自动探测 host,然后将 host list 中目标主机(即想要欺骗的 victim)加入 target1,将 gateway 加入 target2。注意同一主机在 list 里可能有 IPv4 和 IPv6 两个表项,看 mac 地址可以区分(同一主机 mac 地址相同)。Ettercap 中的 host list如何才能确定这些 IP 谁是谁的呢?对于 victim 的 IP,可以直接在 victim 设备的设置里查看,网络设置中会显示自己被分配到的 IP。有些时候,这个 list 里的 description 会显示设备名称。对于 gateway 的 IP,则可以在任何连接该局域网的设备设置里看到,显示的「网关 IP」即是。这个 list 中的 192.168.86.236 就是我们的 victim。配置好 target 后,点击 MITM menu 里的 ARP poisoning 就可以启动 ARP 欺骗。这时候在 Wireshark 里就能抓到局域网内其他主机的包了。使用 Wireshark 抓包在 Wireshark 里筛选相应 IP 的包,就可以看到与 victim 有关的包。可以这么写查询语句:ip.addr == 192.168.86.236。嗅探到的有关 victim 的所有包可以看到各种协议的流量,TCP、ARP、MDNS、HTTP……观察截图中的 48 号开始的数据包,可以发现这完整演示了 TCP 三次握手的过程:48 号 victim 向服务器 61.187.64.7 发送 SYN=1 的包,结果 49 号重传了(可能因为网络问题没有发送成功),这就是第一次握手;51 号,服务器向 victim 发回了 SYNACK 的包,这就是第二次握手,结果这个包又重传了;53 号则是 victim 最后向服务器发送了 SYN=0 的 ACK 包,这就是第三次握手,TCP 连接建立成功!这个 TCP 连接用于什么目的呢?可以看到接下来对于这个服务器的一个请求就是 HTTP GET,也就是 victim 访问了湖大统一身份认证页面的网页。至此,我们直观地看到了从建立 TCP 连接到发送 HTTP 请求的全过程。还注意到,这个连接由 victim 的 60431 端口和服务器的 80 端口建立。在下面的其他数据包里,victim 的端口各不相同,端口号都大于 60000。看来,发送端的确是任意选择一个不定端口进行发送的。实践:湖大统一身份认证登陆现在还有什么现代网站使用 HTTP 吗?答案是肯定的,比如:湖南大学「统一身份认证」的登录页面。这个页面是完全不支持 HTTPS。那么理论上来说,我是不是可以看到在相同局域网登陆的同学的密码呢?就 TM 你没用 HTTPS 是吧?将前面的一切都配置好之后,在同一个局域网下,在目标设备上登录,筛选 Wireshark 的条件为发自 victim 的 IP 和使用 HTTP 协议,可以写 ip.src == 192.168.86.236 && http。确实看到了目标设备浏览器发出的 HTTP 请求!查看包内容,可以看到表单发送的四个字段,其中赫然包含着这个 password:捕获 victim 登录湖大统一认证的请求然而查看发现,这个内容是加密的。结合之前对湖大登录认证的研究,在发送表单之前会对 password 进行 RSA 加密,加密的公钥是加载网页之初向服务器请求的(每次都不同),而对应的密钥只有服务器后端知道。所以,我们只能截获加密后的内容,没有服务器端的密钥就是没法解密的。看来,虽然没有使用 HTTP,但是湖大统一认证的密码登录还是相对安全的。值得注意的是,统一身份认证的登录页面使用了这种比较精巧的设计,利用非对称加密、每次生成不同的密钥对等来确保我们抓到的包看不到密码(也可能是因为有这样一套 HTTP 下也相对安全的机制,信息化办才不愿意升级 HTTPS),然而大多数的登录表单并不会采用这样复杂的设计,绝大多数还是直接明文发送密码或者密码哈希。如果不使用 HTTPS,确实会有很大的安全性问题。校园网的 AP 隔离马上我们就可以看到「在湖大登录个人门户」是「安全的」的另一个原因:校园网的 AP 隔离。如果两个设备都连在校园网,它们彼此之间是无法访问的,并不像一般的局域网一样。正因如此,校园网内不能联机玩 Minecraft。所以,连接湖大校园网 Wi-Fi 时是无法进行这个实验的,即使进行了 ARP 欺骗,Wireshark 也无法嗅探到局域网其他主机发送的包,只能看到一些无意义的 MDNS 查询:校园网中只能嗅探到一些无意义的 MDNS 查询所以校园网内嗅探是不可行的。HTTPS 下能嗅探吗?如果主机使用 HTTPS 访问网站,我们还能看到其发送的请求吗?仍然使用之前嗅探的实验环境,在 victim 上访问 HTTPS 网站进行登录操作,比如学习通:学习通的登录页面可以看到这个页面是使用 HTTPS 连接的。在这个页面输入账号密码登录,用 Wireshark 能够嗅探到:和学习通服务器建立 HTTPS 连接的过程我们知道 HTTPS 建立在服务器的 443 端口。可以看到,我们抓取到了 victim 和超星学习通服务器建立连接的 TCP 包(也就是发向 443 端口的这一系列包),但是在这之后发的包并没有使用 HTTP 协议(或者 HTTPS 协议?),而是使用了 TLSv1.2 协议。TLS 的全称是 Transport Layer Security,顾名思义是个加密协议。事实上对于这些使用 TLS 加密发送的数据包,我们查看只能看到一堆乱码。比较诡异的是,学习通登录之后自动重定向到了一个 HTTP 页面,所以上图最后几行开始又是 HTTP 协议……不知道这垃圾平台的开发者是怎么想的。如果筛选所有 HTTP 包,就只能看到登录成功、重定向之后 GET 的那些内容,看不到表单登陆 POST 账号密码的内容。筛选 HTTP 包,只能看到 HTTP GET综上所述,HTTPS 实际是「HTTP over TLS/SSL」,也就是用 TLS 这类加密协议传输的 HTTP,所以在 Wireshark 里捕获不到 HTTP 的请求内容,只能看到一个个加密后的 TLS 包。
2023年05月13日
109 阅读
0 评论
0 点赞
2023-05-13
CSAPP 2e Datalab 题解
CSAPP 是 CMU 享誉全球的课程,尤其以其 lab 难度之高著称。这是课程的第一个 lab:datalab,为了让我们熟练掌握计算机中的数据存储而设计。前言:什么是 cheating之前在 B 站上 CSAPP 第一节课,教授就说了在 CMU,作弊的定义是什么,说实话有点 shocked:Cheating: DescriptionCheating: ConsequenceCMU CSAPP 的规定是,完成 lab 的时候不可以上网查资料,只要上网搜索了,不管有没有找结果,搜索的这个过程就算作 cheating(这只是课程实验!)。一个学期有 25 人因为 cheating 受到惩罚,而且后果十分严重。看看我们的 HNU,至今没听说过有谁因为作弊而受到处分……按照这个要求,lab 做得真的非常痛苦。有些题目要想很久还想不出来。而且说实话还是有一两个题目不会做,去网上看了别人的 solution。但是这样做完的 datalab,相比直接上网一通搜索抄来的代码,确实更加有成就感,也对自己的「二进制思维」能力有更大的锻炼。这也正是这几个 lab 的意义所在吧。所以,如果你在做 datalab 而上网搜索看到了这里,在继续看下去之前,不妨再继续独立思考一下吧。本实验的一些坑dlc 这个语法检查器按照 C89/C90(ANSI C)的标准,这个标准规定所有局部变量都要在代码块的开头定义。所以如果在函数中途定义一个局部变量,虽然用 make 可以正常编译(现在的编译器很少还用 ANSI C 标准),但是用 dlc 检查则会提示 parse error 的错误。实验环境必须是旧版本的 Linux 环境(如 Ubuntu 12.04 LTS)。使用较新的发行版可能会导致无法通过 fitsBits 这一关。(目前暂不清楚是编译器还是内核等的问题,据说是本 lab 本身的一个 bug)用 docker 创建运行环境在 x86 的主机上使用 docker 创建 Ubuntu 12.04 LTS 的容器,指定将容器内 /home 目录映射到容器外的目录,然后进入容器,安装相关软件:将容器内 /home 映射到容器外 /root/Lab/HNU-CSAPP/ex2/docker-env/home,这两个目录也可以自己指定docker run -itd --name datalab -v /root/Lab/HNU-CSAPP/ex2/docker-env/home:/home ubuntu:12.04进入容器内的 shelldocker exec -it datalab /bin/bash12.04 年代久远,需要将 sources 里 archive.ubuntu.com 改为 old-reloease.ubuntu.com 才能 apt-get updatesed -i -e 's/archive.ubuntu.com|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list获取软件包列表apt-get update安装本实验必要的软件包apt-get install gcc make gcc-multilib回到容器外,将文件移进指定的目录(/root/Lab/HNU-CSAPP/ex2/docker-env/home),编辑好后再进入容器测试。可以用 tmux 半屏开 vim 写代码,半屏进容器终端测试。注意如果在容器外测试过了,进入容器要 make clean 再重新 make,因为不同环境编译生成的二进制可执行文件可能不兼容。bitAnd用或和非实现与,用直接取反的方法即可:x and y = not ((not x) or (not y))。getByte很显然答案是 (x >> n8) & 0xFF。但是不允许使用乘法,如何获得 n8 呢?答案是直接用加法。8 个 n 相加会超过 ops 数量限制,我们可以先加出 2n、4n。logicalShift要求实现 logical shift,因为默认的 shift 是 arithmetic shift。本质的区别在于负数的 shr,前者左边出来的是 0 而右者出来的是 1。很容易想到可以 and 上一个 keeper,这个 keeper 的值是 0000111...11,右边的 1 就是要保留的那些位。这样可以去掉左边产生的 1。如何得到这个 keeper?显然 keeper = 0x7FFFFFFF >> (n-1)。有两个问题。首先是如何产生 0x7FFFFFFF 这个数字呢?因为游戏规则规定立即数赋值不能超过 2 Byte。答案是 ~(1 << 31)。接下来是如何处理 n = 0 的情况。好在这关允许我们用 ! 运算符,可以实现类似选择赋值的语句。具体来说,我们需要这样一个 flag 变量,当 n 为 0 时它是 0x00000000,当 n 非 0 时它是 0xFFFFFFFF。试试用 ! 运算符,直接对 n 取逻辑非 notn,这样 n 为 0 时我们得到 1,n 非 0 时我们得到 0。很显然,flag = notn - 1。现在我们分别计算出 n 为 0 和不为 0 的两种情况 result1 和 result2,则最后只需要返回 ((!flag) & result1) | (flag & result2)。是不是有点像数电里的「多路复用」呢。这种对 n = 0 特判的方法,在后面的关卡中会多次遇到。最后,这关不能用 - 减法,如何实现 -1 呢?直接用加上 0xFFFFFFFF 就行啦。bitCount(这题有点难想 😨)要求统计二进制中 1 的个数(就是 popcount)。其实对于每一位是 0 是 1 我们都可以通过 and 得知,最难的就是如何累加。累加的部分可以考虑分治,要累加 32 位的结果,我们假设得到了两个 16 位的结果,只考虑这两个如何相加;要计算 16 位的结果只考虑两个 8 位的结果如何相加,以此类推。那么两个 16 位的结果如何相加呢?我们假设两个结果分别存储在 32 位 int 的高 16 位和低 16 位,只要把两个部分取出来,前者右移 16 位相加即可。下面的也类似。除此之外,题目要求使用的立即数大小不超过 0xFF,需要用一些 tricky 的手段获得我们要的立即数,以免超出符号数量限制。可以参阅代码。bang要求计算逻辑反,也就是我们要返回这样一个值,当 x 为全 0 时其为 1,x 任何一位为 1 时其为 0。理所当然地可以想到应该把 x 每一位 or 起来(或者把 ~x 每一位 and 起来),结果放在最低位,得到 1 或 0。然而如果真的取出 32 位每一位进行 or,ops 会超过限制。可以用分治的方法,先将 [0,15] 和 [16,31] 这两段按位处理放入低的一段,再处理 [0,7] 和 [8,15],以此类推,可以在 ops 数量限制内完成。tmin返回最小的 int,就是 1 << 31。fitsBits第一种方法是一开始想的奇怪方法,至少需要用到 19 个 ops,超过题目限制,其实不能通过。要我们返回 0 或 1 表示 x 是否能被 n 位补码表示,实际上就是返回是否 −2�−1≤�≤2�−1−1−2 n−1 ≤x≤2 n−1 −1。也就是 −2�−1≤�<0−2 n−1 ≤x<0 或 0≤�≤2�−1−10≤x≤2 n−1 −1。再变换一下,就是:0≤�+2�−1<2�−10≤x+2 n−1 <2 n−1 或 −2�−1≤�−2�−1≤−1−2 n−1 ≤x−2 n−1 ≤−1。也就是如果 �<0x<0 则要满足 �+2�−1≥0x+2 n−1 ≥0;如果 �≥0x≥0 则要满足 �−2�−1≤−1x−2 n−1 ≤−1。依然根据 x 的符号位选择一个结果返回。第二种方法则是可以通过的正解:根据算数移位的性质。假设给我们一个 n 位补码表示的数,我们直接将其左移至与 32 位 int 符号位对齐,然后再移回去,得到的结果应该是和原来一样的。据此即可判断。⚠️ 这题使用较新版本的环境测试是无法通过的,目前暂不清楚与什么因素有关。在 Ubuntu 12.04 LTS x86_64 中测试可以通过。详见开头「用 docker 创建运行环境」。divpwr2要求返回 �2�2 nx ,向 0 取整。对于正数当然就是 >> n 即可,对于负数就有些复杂,因为 >> 运算默认向下取整,需要进行修正。对于负数,列表可以发现,每个数字在计算之前要加上 2�−12 n −1 的偏移才能得到正确答案。因为出现了 n - 1,所以还要特别考虑 n = 0 的情况。使用 logicalShift 一样的「多路复用」方法即可。negate取负数,就是 (~x)+1,很简单。isPositive根据符号位,可以轻易判断出这个数是否 >= 0。既然本题要求判断 > 0,也就是 0 是特殊情况,依然可以根据前面的方法特殊判断。isLessOrEqual判断 �≤�x≤y,不能直接判断 �−�≥0y−x≥0,因为会 overflow。可以发现如果 x 和 y 同号,肯定不会 overflow。所以只需要对异号的情况进行判断。为了更加清晰,列出一个真值表:x 符号位 y 符号位 返回值1 0 10 1 00 0 y-x 的符号位取反1 1 y-x 的符号位取反依然是个分类讨论(「多路复用」)的思路,根据 x xor y 的值返回答案,如果为 1 答案是 x 的符号位,如果为 0 答案为 y-x 的符号位取反。ilog2要求返回 log2(x) 向下取整。很显然答案就是 x 的二进制中最左边的 1(最高位)在从右往左第几位。首先通过将 x 按位或 x >> k 的手段(k 分别等于 1、2、4……),可以将 x = 0001xxxxx 变为 x = 000111111,也就是最高位之后全都变成 1。然后可以做一遍之前的 bitCount,将结果减去 1 即可。float_neg这部分终于可以用 if 了。只要判断 NaN 的情况原封不动返回,否则修改符号位返回就行。对符号位取反可以对 1<<31 取异或。可以构造两个变量 get_exp 和 get_frac 分别用于和一个数按位与,从而取出这个数中的 exp 或 frac 片段。exp 就是 0xFF << 23,frac 则是 ~exp 再对符号位取反。float_i2f要将提供的 int 值 x 转换为 float。int 和 float 关于负数的概念是截然不同的,所以我们必须对 int 的绝对值进行处理。所以首先要对于 x < 0 的情况设置符号位后直接取相反数。(此时注意特判 tmin 的情况!它没有能由 int 表示的相反数)设置好 sign 后,只要处理 exp 和 frac 即可。假设 x 是个 n 位的二进制数,exp 就等于 n - 1 + bias。frac 就是右边的 n - 1 位(左对齐)。要考虑进位问题。将 frac 被截掉的部分拿出来(最多有 8 位),如果「大于 0x80」或者「等于 0x80 且frac 末位为 1」(round to even 的情况)则要进一位,frac 连续进位。注意到 frac 有可能进位进到 exp 里,但是我们的代码里不用判断,因为 frac 最高位之前就是 exp 最低位。(在这里是不是再次感到了 IEEE 浮点数设计的精妙?)最后注意这题 0 也要特判,因为 0 属于 denomilized。这题写起来很容易超过 ops 数量限制。需要省着点用。float_twice将给出的 float 翻两倍,要分别考虑 nomalized、denomolized 和 special 的情况。nomolized 很简单,直接修改 exp 部分即可。注意可能有 nomolized 进入 infinity 的情况。denomolized 只需要修改 frac 即可。注意可能有从 denomolized 进入 nomolized 的情况,要修改 exp。最后 infinity、NaN 这两种值都原样返回,需要特判。完整代码参考/*bitAnd - x&y using only ~ and |Example: bitAnd(6, 5) = 4Legal ops: ~ |Max ops: 8Rating: 1 */int bitAnd(int x, int y) { return ~((~x) | (~y));}/*getByte - Extract byte n from word xBytes numbered from 0 (LSB) to 3 (MSB)Examples: getByte(0x12345678,1) = 0x56Legal ops: ! ~ & ^ | + << >>Max ops: 6Rating: 2 */int getByte(int x, int n) { int n2 = n + n; int n4 = n2 + n2; int n8 = n4 + n4; return (x >> n8) & 0xFF;}/*logicalShift - shift x to the right by n, using a logical shiftCan assume that 0 <= n <= 31Examples: logicalShift(0x87654321,4) = 0x08765432Legal ops: ! ~ & ^ | + << >>Max ops: 20Rating: 3 */int logicalShift(int x, int n) { int sub1 = ~0; int keeper = ~(1 << 31); int result = (x >> n) & (keeper >> (n + sub1)); int notn = !n; int flag = notn + sub1; return ((~flag) & x) | (flag & result);}/*bitCount - returns count of number of 1's in wordExamples: bitCount(5) = 2, bitCount(7) = 3Legal ops: ! ~ & ^ | + << >>Max ops: 40Rating: 4 */int bitCount(int x) { int ret = x; int mask = 0; // 0x55555555 mask = 0x55 + (0x55 << 8); mask = mask + (mask << 16); ret = (ret & mask) + ((ret>>1) & mask); // 0x33333333 mask = 0x33 + (0x33 << 8); mask = mask + (mask << 16); ret = (ret & mask) + ((ret>>2) & mask); // 0x0f0f0f0f mask = 0x0f + (0x0f << 8) + (0x0f << 16) + (0x0f << 24); ret = (ret & mask) + ((ret>>4) & mask); // 0x00ff00ff mask = 0xff + (0xff << 16); ret = (ret & mask) + ((ret>>8) & mask); // 0x0000ffff mask = 0xff + (0xff << 8); ret = (ret & mask) + ((ret>>16) & mask); return ret;}/*bang - Compute !x without using !Examples: bang(3) = 0, bang(0) = 1Legal ops: ~ & ^ | + << >>Max ops: 12Rating: 4 */int bang(int x) { int ret = ~x; ret = ret & (ret >> 16); ret = ret & (ret >> 8); ret = ret & (ret >> 4); ret = ret & (ret >> 2); ret = ret & (ret >> 1); return ret&1;}/*tmin - return minimum two's complement integerLegal ops: ! ~ & ^ | + << >>Max ops: 4Rating: 1 */int tmin(void) { return 1 << 31;}/*fitsBits - return 1 if x can be represented as ann-bit, two's complement integer.1 <= n <= 32Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1Legal ops: ! ~ & ^ | + << >>Max ops: 15Rating: 2 */int fitsBits(int x, int n) {// int sub1 = ~0;// int get_sign = 1<<31;// int sign = x & get_sign;// int not_sign = !sign;// int power = 1 << (n + sub1);// int sub_power = (~power) + 1;// int result1 = !(((x + power) & get_sign) >> 31);// int result2 = ((x + sub_power) & get_sign) >> 31;// sign = !not_sign;// return (sign & result1) | (not_sign & result2); int subn = (~n) + 1; int to_shl = 32 + subn; int x_fix = (x << to_shl) >> to_shl; int not_ans = x ^ x_fix; return !not_ans;}/*divpwr2 - Compute x/(2^n), for 0 <= n <= 30Round toward zeroExamples: divpwr2(15,1) = 7, divpwr2(-33,4) = -2Legal ops: ! ~ & ^ | + << >>Max ops: 15Rating: 2 */int divpwr2(int x, int n) {// Round to zero is the difficult part int sub1 = ~0; int super2sub1 = (1<<n) + sub1; int notn = !n; int flag = notn + sub1; int result = (x + ((x >> 31) & super2sub1)) >> n; return ((~flag) & x) | (flag & result);}/*negate - return -xExample: negate(1) = -1.Legal ops: ! ~ & ^ | + << >>Max ops: 5Rating: 2 */int negate(int x) { return (~x) + 1;}/*isPositive - return 1 if x > 0, return 0 otherwiseExample: isPositive(-1) = 0.Legal ops: ! ~ & ^ | + << >>Max ops: 8Rating: 3 */int isPositive(int x) { int sub1 = ~0; int notx = !x; int flag = notx + sub1; int result = !(x >> 31); return flag & result; // return ((~flag) & 0) | (flag & result); // the first part isn't necessary}/*isLessOrEqual - if x <= y then return 1, else return 0Example: isLessOrEqual(4,5) = 1.Legal ops: ! ~ & ^ | + << >>Max ops: 24Rating: 3 */int isLessOrEqual(int x, int y) { int xxory = (x >> 31) ^ (y >> 31); int subx = (~x) + 1; int ysubx = y + subx; int result_1 = (x >> 31) & 1; int result_2 = !(ysubx >> 31); return (xxory & result_1) | ((~xxory) & result_2);}/*ilog2 - return floor(log base 2 of x), where x > 0Example: ilog2(16) = 4Legal ops: ! ~ & ^ | + << >>Max ops: 90Rating: 4 */int ilog2(int x) { int sub1 = ~0; int x_fill = x; int ret, mask; x_fill |= x_fill >> 1; x_fill |= x_fill >> 2; x_fill |= x_fill >> 4; x_fill |= x_fill >> 8; x_fill |= x_fill >> 16;// perform bitCount ret = x_fill; mask = 0; // 0x55555555 mask = 0x55 + (0x55 << 8); mask = mask + (mask << 16); ret = (ret & mask) + ((ret>>1) & mask); // 0x33333333 mask = 0x33 + (0x33 << 8); mask = mask + (mask << 16); ret = (ret & mask) + ((ret>>2) & mask); // 0x0f0f0f0f mask = 0x0f + (0x0f << 8) + (0x0f << 16) + (0x0f << 24); ret = (ret & mask) + ((ret>>4) & mask); // 0x00ff00ff mask = 0xff + (0xff << 16); ret = (ret & mask) + ((ret>>8) & mask); // 0x0000ffff mask = 0xff + (0xff << 8); ret = (ret & mask) + ((ret>>16) & mask); return ret + sub1;}/*float_neg - Return bit-level equivalent of expression -f forfloating point argument f.Both the argument and result are passed as unsigned int's, butthey are to be interpreted as the bit-level representations ofsingle-precision floating point values.When argument is NaN, return argument.Legal ops: Any integer/unsigned operations incl. ||, &&. also if, whileMax ops: 10Rating: 2 */unsigned float_neg(unsigned uf) { unsigned change_sign = 1 << 31; unsigned get_exp = 0xFF << 23; unsigned get_frac = (~get_exp) ^ change_sign; if ((uf & get_exp) == get_exp && (uf & get_frac) != 0) return uf; return uf ^ change_sign;}/*float_i2f - Return bit-level equivalent of expression (float) xResult is returned as unsigned int, butit is to be interpreted as the bit-level representation of asingle-precision floating point values.Legal ops: Any integer/unsigned operations incl. ||, &&. also if, whileMax ops: 30Rating: 4 */unsigned float_i2f(int x) { int bias = 127; unsigned get_frac = 0x007fffff; unsigned get_frac_cut = 0x000000ff; unsigned tminf = 0xcf000000; int fix_x, n, nsub1, temp; unsigned sign, exp, frac, frac_cut;// special cases if (x == 0x80000000) return tminf; if (x == 0) return 0;// get abs / get sign fix_x = x; sign = 0; if (fix_x < 0) fix_x = -fix_x, sign = 0x80000000;// get count n = 0; temp = fix_x; while (temp) n++, temp>>=1; nsub1 = n - 1; //printf("count=%d\n",count);// get exp exp = ((nsub1 + bias) << 23);// get frac if (nsub1 <= 23){frac = (fix_x << (23 - nsub1)) & get_frac;} else {frac = (fix_x >> (nsub1 - 23)) & get_frac; frac_cut = (fix_x << (31 - nsub1)) & get_frac_cut; if ((frac_cut > 0x80) || (frac_cut == 0x80 && (frac & 1))) frac++; // carry}return sign + exp + frac;}/*float_twice - Return bit-level equivalent of expression 2*f forfloating point argument f.Both the argument and result are passed as unsigned int's, butthey are to be interpreted as the bit-level representation ofsingle-precision floating point values.When argument is NaN, return argumentLegal ops: Any integer/unsigned operations incl. ||, &&. also if, whileMax ops: 30Rating: 4 */unsigned float_twice(unsigned uf) { unsigned neg_inf = 0xff800000; unsigned pos_inf = 0x7f800000; unsigned get_exp = 0x7f800000; unsigned get_frac = 0x007fffff; unsigned exp = (uf & get_exp) >> 23; unsigned frac = uf & get_frac; unsigned ret = uf; if (exp == 0xff) return uf; if (exp == 0){ // denomolizedif (frac & (1<<23)){ // de -> no ret = ret | (1 << 23); return ret; } else { ret = ret - frac; frac = frac << 1; ret = ret + frac; return ret; }} else { // nomolizedret = ret - (exp << 23); exp++; if (exp > 0xff) { // no -> inf if (uf & (1<<31)) return neg_inf; else return pos_inf; } ret = ret + (exp << 23); return ret;}}
2023年05月13日
124 阅读
0 评论
0 点赞
2023-05-13
部署 Memos 接入 QQ 机器人
前几天部署了开源的浮墨笔记(flomo)替代品:Memos。这个项目已经有了 Android、iOS 客户端、浏览器拓展等等一系列生态。但是对比原版的浮墨笔记,少了 QQ / 微信的接入对于我来说不太方便。Memos 提供了简便的 API,我们可以自己动手,部署一个 QQ 机器人。前置准备服务器,最好是境内的。搭建好的 Memos 实例(我的实例:memos.skywt.cn)。一个可登录的闲置 QQ 账号。运行 go-cqhttp我们的机器人在服务端分为两个组件,这两个组件都要部署在我们的服务器上。go-cqhttp 负责与腾讯 QQ 服务器通信,管理我们的账号登录、消息收发等过程;NoneBot 负责将 go-cqhttp 收到的消息进行处理,控制发送消息的内容,运行我们想要的业务逻辑。首先部署 go-cqhttp。前往 GitHub 上的 release 下载最新适合服务器架构的版本,下载解压之后是一个单文件程序。第一次运行,在提示选择通信方式时,选择「反向 Websocket 通信」,之后程序就会在相同目录下生成配置文件。编辑配置文件,根据提示将 uin 和 password 字段分别改为自己的 QQ 和密码。虽然配置文件中说「密码为空时可以使用扫码登录」,但是经过实测只有在同一网络环境下 QQ 才会允许扫码登录。部署在服务器上扫码是无法登录的,只能使用密码。除此之外,修改文件底部 servers 部分 ws-reverse 中 universal 的地址。我设定的是 ws://127.0.0.1:8989/ws/。这个地址告诉 go-cqhttp,该从哪里与后端的 NoneBot 通信。修改完毕后,再次启动 go-cqhttp,可能会提示需要扫码、滑块验证、手机验证之类,跟着程序的提示进行即可。程序会提示,对于腾讯云的服务器需要修改 DNS 为 114.114.114.114。编辑 /etc/resolv.conf 并修改就行。出现以下消息就表明登录成功:此时可能会出现大量的「Connection Refused」消息,这是因为我们还没启动后端的 NoneBot。忽略他们即可。接下来,要将 go-cqhttp 放到后台运行,开始编写 NoneBot 的主程序。编写运行 NoneBot参阅 NoneBot 的官方指南编写主要的代码,包括 bot.py 和 config.py。在后者中要指定与 go-cqhttp 的 config.yml 中配置一致的 HOST 和 PORT。我的配置分别是 127.0.0.1 和 8989。接下来,我们可以通过编写插件的方式使 QQ 机器人实现 Memo 备忘录的功能。其实直接根据文档里的插件修改一下就可以了:from nonebot import on_command, CommandSessionimport requestsimport jsonplugin_name = 'Memos'plugin_usage = r"""memo [需要添加的 Memo 内容]"""MEMOS_API = 'https://memos.skywt.cn/api/memo?openId='@on_command('memo', aliases=('note'))async def memo(session: CommandSession):memo_text = session.current_arg_text.strip() if not memo_text: await session.send('Memo 内容不能为空!') return ret = await send_memo(memo_text) await session.send(ret) async def send_memo(memo_text: str) -> str:data_to_send = {'content': memo_text} r = requests.post(MEMOS_API, data=json.dumps(data_to_send)) if r.status_code == 200: return '添加成功' else: return '添加失败,请查阅日志'回到 NoneBot 的主目录启动 bot.py 并放在后台,发送消息 memo 这是一条测试笔记,可以看到正确的输出。进入 Memos 可以看到成功添加了一条 memo。聊天界面,机器人回复添加成功参考资料go-cqhttp 使用指南NoneBot 使用指南:文档通俗易懂,强烈推荐
2023年05月13日
135 阅读
0 评论
0 点赞
1
2