<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>二进制安全 - 她和她的猫</title>
    <link>https://her-cat.com/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%AE%89%E5%85%A8/</link>
    <description>二进制安全的文章列表 - 她和她的猫</description>
    <image>
      <title>她和她的猫</title>
      <url>https://her-cat.com/assets/favorite.jpeg</url>
      <link>https://her-cat.com/assets/favorite.jpeg</link>
    </image>
    <generator>Hugo -- 0.147.5</generator>
    <language>zh</language>
    <lastBuildDate>Thu, 25 Mar 2021 12:42:22 +0800</lastBuildDate>
    <atom:link href="https://her-cat.com/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%AE%89%E5%85%A8/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>什么是二进制安全</title>
      <link>https://her-cat.com/posts/2021/03/25/what-is-binary-safe/</link>
      <pubDate>Thu, 25 Mar 2021 12:42:22 +0800</pubDate>
      <guid>https://her-cat.com/posts/2021/03/25/what-is-binary-safe/</guid>
      <description>众所周知，C 语言中使用字符数组来表示字符串，并在字符串末尾使用空字符 \0 标识字符串结束。 如果字符串中包含 \0 或者二进制数据，就会导致</description>
      <content:encoded><![CDATA[<p>众所周知，C 语言中使用字符数组来表示字符串，并在字符串末尾使用空字符 <code>\0</code> 标识字符串结束。</p>
<p>如果字符串中包含 <code>\0</code> 或者二进制数据，就会导致 strlen 函数获取的长度跟字符串实际的长度不一致。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">int main()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    char *str1 = &#34;hello her-cat&#34;;
</span></span><span class="line"><span class="cl">    char *str2 = &#34;hello\0her-cat&#34;;
</span></span><span class="line"><span class="cl">    char *str3 = &#34;hello\x00her-cat&#34;;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    printf(&#34;str1: %lu\n&#34;, strlen(str1));
</span></span><span class="line"><span class="cl">    printf(&#34;str2: %lu\n&#34;, strlen(str2));
</span></span><span class="line"><span class="cl">    printf(&#34;str3: %lu\n&#34;, strlen(str3));
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    return 1;
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 输出：
</span></span><span class="line"><span class="cl">// str1: 13
</span></span><span class="line"><span class="cl">// str2: 5
</span></span><span class="line"><span class="cl">// str3: 5
</span></span></code></pre></div><p>可以看到 str2 和 str3 都只统计了 hello 的长度，为什么会出现这种情况？</p>
<p>因为 strlen 函数是通过遍历字符串来计算长度的，时间复杂度为 O(n)。在遍历的过程中，如果某个字符等于 <code>\0</code>， 就会停止遍历并返回第一个字符到该字符的字符数量，也就是字符串的长度。</p>
<p>当字符串中包含 <code>\0</code> 就会导致提前停止遍历，导致得到错误的字符串长度。所以我们称 C 语言中的 strlen 函数是非二进制安全的。</p>
<p>PHP 的 strlen 函数是二进制安全的，因为它不依赖于 <code>\0</code> 确定字符串的长度。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">&lt;?php
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$str1 = &#34;hello her-cat&#34;;
</span></span><span class="line"><span class="cl">$str2 = &#34;hello\0her-cat&#34;;
</span></span><span class="line"><span class="cl">$str3 = &#34;hello\x00her-cat&#34;;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">printf(&#34;str1: %lu\n&#34;, strlen($str1));
</span></span><span class="line"><span class="cl">printf(&#34;str2: %lu\n&#34;, strlen($str1));
</span></span><span class="line"><span class="cl">printf(&#34;str3: %lu\n&#34;, strlen($str1));
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 输出：
</span></span><span class="line"><span class="cl">// str1: 13
</span></span><span class="line"><span class="cl">// str2: 13
</span></span><span class="line"><span class="cl">// str3: 13
</span></span></code></pre></div><p>在 PHP 字符串结构体中，使用了 len 字段保存字符串的长度，调用 strlen 时读取该值就能得到字符串的长度。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">struct _zend_string {
</span></span><span class="line"><span class="cl">	zend_refcounted_h gc; // 垃圾回收信息
</span></span><span class="line"><span class="cl">	zend_ulong        h; // hash 值
</span></span><span class="line"><span class="cl">	size_t            len; // 字符串长度
</span></span><span class="line"><span class="cl">	char              val[1]; // 柔性字节数组，保存字符串
</span></span><span class="line"><span class="cl">};
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 读取字符串长度的宏
</span></span><span class="line"><span class="cl">#define ZSTR_LEN(zstr)  (zstr)-&gt;len
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ZEND_FUNCTION(strlen)
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">	zend_string *s;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	// 解析 strlen 的参数 s
</span></span><span class="line"><span class="cl">	// 1, 1 表示最小参数个数和最大参数个数
</span></span><span class="line"><span class="cl">	ZEND_PARSE_PARAMETERS_START(1, 1)
</span></span><span class="line"><span class="cl">		Z_PARAM_STR(s)
</span></span><span class="line"><span class="cl">	ZEND_PARSE_PARAMETERS_END();
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	// 返回字符串长度
</span></span><span class="line"><span class="cl">	RETVAL_LONG(ZSTR_LEN(s));
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>《Redis 设计与实现》中解释了什么是二进制安全：</p>
<blockquote>
<p>SDS 的 API 都是二进制安全的（binary-safe）： 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据，<strong>程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的， 它被读取时就是什么样</strong>。</p></blockquote>
<p>为什么 C 语言的 strlen 函数不是二进制安全的？</p>
<p>因为它依赖字符串中的数据，对字符串中的数据做了过滤，只要字符串出现 <code>\0</code> 就认为字符串结束，导致计算长度时读取到的数据跟写入的数据不一致。</p>
<p>那么 Redis 是怎么解决二进制安全的问题呢？</p>
<p>在书中也有提到，由于直接使用 C 字符串存在二进制安全问题，所以不能用 C 字符串保存二进制数据（文本、图片）。</p>
<p>Redis 作为一个 KV 数据库，肯定不能限制用户存取的数据的类型，为了解决这个问题，Redis 设计了 SDS（Simple Dynamic String） 数据结构，又称简单动态字符串，结构体如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">struct sdshdr {
</span></span><span class="line"><span class="cl">    // 记录 buf 数组中已使用字节的数量
</span></span><span class="line"><span class="cl">    // 等于 SDS 所保存字符串的长度
</span></span><span class="line"><span class="cl">    int len;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 记录 buf 数组中未使用字节的数量
</span></span><span class="line"><span class="cl">    int free;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    // 字节数组，用于保存字符串
</span></span><span class="line"><span class="cl">    char buf[];
</span></span><span class="line"><span class="cl">};
</span></span></code></pre></div><blockquote>
<p>这里用的是 Redis 3.0 版本的 SDS，新版本的 Redis 对 SDS 进行了优化，结构体也有变化。</p></blockquote>
<p>与 PHP 处理的方式相同，也是通过 len 的字段用来保存字符串的长度，不仅避免了二进制安全问题，同时提高了获取字符串长度的效率，不需要每次都去遍历 buf 字节数组。</p>
<p>同时，SDS 相对于 C 字符串具有以下优点：</p>
<ul>
<li>常数复杂度获取字符串长度</li>
<li>杜绝缓冲区溢出</li>
<li>减少修改字符串时带来的内存重分配次数</li>
<li>二进制安全</li>
<li>兼容部分 C 字符串函数</li>
</ul>
<p>关于 SDS 完整的实现可以查看 <a href="http://redisbook.com/preview/sds/different_between_sds_and_c_string.html">SDS 与 C 字符串的区别</a>。</p>
<p>上面例子都是关于字符串的，这里再写一个二进制数据的例子。</p>
<p>首先用 PHP 生成二进制数据写入到文件中，并打印出来。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="o">&lt;</span><span class="err">?</span><span class="n">php</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">$</span><span class="nb">str</span> <span class="o">=</span> <span class="n">sprintf</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">%s%s%s</span><span class="s1">&#39;</span><span class="p">,</span> <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;N&#39;</span><span class="p">,</span> <span class="mi">123</span><span class="p">),</span> <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;n&#39;</span><span class="p">,</span> <span class="mi">456</span><span class="p">),</span> <span class="n">pack</span><span class="p">(</span><span class="s1">&#39;n&#39;</span><span class="p">,</span> <span class="mi">789</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">file_put_contents</span><span class="p">(</span><span class="s1">&#39;data.dat&#39;</span><span class="p">,</span> <span class="o">$</span><span class="nb">str</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">var_dump</span><span class="p">(</span><span class="o">$</span><span class="nb">str</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="err">输出：</span>
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="n">string</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="s2">&#34;{�&#34;</span>
</span></span></code></pre></div><p>因为 PHP 对字符串读写是二进制安全的，所以能够正确打输出长度和内容。</p>
<p>接下来用 C 语言读取 data.dat 文件并输出。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">int main()
</span></span><span class="line"><span class="cl">{
</span></span><span class="line"><span class="cl">    FILE *fp;
</span></span><span class="line"><span class="cl">    char str[8];
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    fp = fopen(&#34;data.dat&#34;, &#34;r&#34;);
</span></span><span class="line"><span class="cl">    fread(&amp;str, 8, 8, fp);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    printf(&#34;len: %lu\n&#34;, strlen(str));
</span></span><span class="line"><span class="cl">    printf(&#34;str: %s\n&#34;, str);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    return 1;
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// 输出：
</span></span><span class="line"><span class="cl">// len: 0
</span></span><span class="line"><span class="cl">// str:
</span></span></code></pre></div><p>意料之中的长度为0、内容为空，用 GDB 调试可以看到 str 其实是有内容的。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">(gdb) b main
</span></span><span class="line"><span class="cl">Breakpoint 1 at 0x792: file /tmp/tmp.8inXlhJrYC/main.c, line 5.
</span></span><span class="line"><span class="cl">(gdb) r
</span></span><span class="line"><span class="cl">Starting program: /home/vagrant/code/her-cat/binary-safe/build/bin/main
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Breakpoint 1, main () at /tmp/tmp.8inXlhJrYC/main.c:5
</span></span><span class="line"><span class="cl">5	{
</span></span><span class="line"><span class="cl">(gdb) u 12
</span></span><span class="line"><span class="cl">main () at /tmp/tmp.8inXlhJrYC/main.c:12
</span></span><span class="line"><span class="cl">12	    printf(&#34;len: %lu\n&#34;, strlen(str));
</span></span><span class="line"><span class="cl">(gdb) p str
</span></span><span class="line"><span class="cl">$1 = &#34;\000\000\000{\001\310\003\025&#34;
</span></span></code></pre></div><p>最后总结一下，二进制安全就是：严格按照二进制的方式进行读写数据，不关心数据的内容，写入是什么样，读取就是什么样。</p>
<p>如果不能做到这些，那就是非二进制安全的。</p>
<p>一个没有感情的二进制数据读写工具。</p>
]]></content:encoded>
    </item><follow_challenge>
      <feedId>58021783493571598</feedId>
      <userId>56882619875632128</userId>
    </follow_challenge>
  </channel>
</rss>
