http://blog.csdn.net/zhangskd/article/details/11770647
分类: Linux TCP/IP Linux Kernel 2013-09-24 18:29 7853人阅读 评论(0) 收藏 举报

目录(?)[+]

概述

TCP校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到

接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。

TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。

TCP的校验和是必需的,而UDP的校验和是可选的。

TCP和UDP计算校验和时,都要加上一个12字节的伪首部。

Author : zhangskd @ csdn blog

伪首部

伪首部共有12字节,包含如下信息:源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。

伪首部是为了增加TCP校验和的检错能力:如检查TCP报文是否收错了(目的IP地址)、传输层协议是否选对了(传输层协议号)等。

定义

(1) RFC 793的TCP校验和定义

The checksum field is the 16 bit one's complement of the one's complement sum of all 16-bit words in the header and text.

If a segment contains an odd number of header and text octets to be checksummed, the last octet is padded on the right

with zeros to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing

the checksum, the checksum field itself is replaced with zeros.

上述的定义说得很明确:

首先,把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。

把TCP报头中的校验和字段置为0(否则就陷入鸡生蛋还是蛋生鸡的问题)。

其次,用反码相加法累加所有的16位字(进位也要累加)。

最后,对计算结果取反,作为TCP的校验和。

(2) RFC 1071的IP校验和定义

1. Adjacent octets to be checksummed are paired to form 16-bit integers, and the 1's complement sum of these

16-bit integers is formed.

2. To generate a checksum, the checksum field itself is cleared, the 16-bit 1's complement sum is computed over

the octets concerned, and the 1's complement of this sum is placed in the checksum field.

3. To check a checksum, the 1's complement sum is computed over the same set of octets, including the checksum

field. If the result is all 1 bits (-0 in 1's complement arithmetic), the check succeeds.

可以看到,TCP校验和、IP校验和的计算方法是基本一致的,除了计算的范围不同。

实现

基于2.6.18、x86_64。

csum_tcpudp_nofold()按4字节累加伪首部到sum中。

  1. static inline unsigned long csum_tcpudp_nofold (unsigned long saddr, unsigned long daddr,
  2. unsigned short len, unsigned short proto,
  3. unsigned int sum)
  4. {
  5. asm("addl %1, %0\n"    /* 累加daddr */
  6. "adcl %2, %0\n"    /* 累加saddr */
  7. "adcl %3, %0\n"    /* 累加len(2字节), proto, 0*/
  8. "adcl $0, %0\n"    /*加上进位 */
  9. : "=r" (sum)
  10. : "g" (daddr), "g" (saddr), "g" ((ntohs(len) << 16) + proto*256), "0" (sum));
  11. return sum;
  12. }

csum_tcpudp_magic()产生最终的校验和。

首先,按4字节累加伪首部到sum中。

其次,累加sum的低16位、sum的高16位,并且对累加的结果取反。

最后,截取sum的高16位,作为校验和。

  1. static inline unsigned short int csum_tcpudp_magic(unsigned long saddr, unsigned long daddr,
  2. unsigned short len, unsigned short proto,
  3. unsigned int sum)
  4. {
  5. return csum_fold(csum_tcpudp_nofold(saddr, daddr, len, proto, sum));
  6. }
  7. static inline unsigned int csum_fold(unsigned int sum)
  8. {
  9. __asm__(
  10. "addl %1, %0\n"
  11. "adcl 0xffff, %0"
  12. : "=r" (sum)
  13. : "r" (sum << 16), "0" (sum & 0xffff0000)
  14. /* 将sum的低16位,作为寄存器1的高16位,寄存器1的低16位补0。
  15. * 将sum的高16位,作为寄存器0的高16位,寄存器0的低16位补0。
  16. * 这样,addl %1, %0就累加了sum的高16位和低16位。
  17. *
  18. * 还要考虑进位。如果有进位,adcl 0xfff, %0为:0x1 + 0xffff + %0,寄存器0的高16位加1。
  19. * 如果没有进位,adcl 0xffff, %0为:0xffff + %0,对寄存器0的高16位无影响。
  20. */
  21. );
  22. return (~sum) >> 16; /* 对sum取反,返回它的高16位,作为最终的校验和 */
  23. }

发送校验

  1. #define CHECKSUM_NONE 0 /* 不使用校验和,UDP可选 */
  2. #define CHECKSUM_HW 1 /* 由硬件计算报头和首部的校验和 */
  3. #define CHECKSUM_UNNECESSARY 2 /* 表示不需要校验,或者已经成功校验了 */
  4. #define CHECKSUM_PARTIAL CHECKSUM_HW
  5. #define CHECKSUM_COMPLETE CHECKSUM_HW

@tcp_transmit_skb()

icsk->icsk_af_ops->send_check(sk, skb->len, skb); /* 计算校验和 */

  1. void tcp_v4_send_check(struct sock *sk, int len, struct sk_buff *skb)
  2. {
  3. struct inet_sock *inet = inet_sk(sk);
  4. struct tcphdr *th = skb->h.th;
  5. if (skb->ip_summed == CHECKSUM_HW) {
  6. /* 只计算伪首部,TCP报头和TCP数据的累加由硬件完成 */
  7. th->check = ~tcp_v4_check(th, len, inet->saddr, inet->daddr, 0);
  8. skb->csum = offsetof(struct tcphdr, check); /* 校验和值在TCP首部的偏移 */
  9. } else {
  10. /* tcp_v4_check累加伪首部,获取最终的校验和。
  11. * csum_partial累加TCP报头。
  12. * 那么skb->csum应该是TCP数据部分的累加,这是在从用户空间复制时顺便累加的。
  13. */
  14. th->check = tcp_v4_check(th, len, inet->saddr, inet->daddr,
  15. csum_partial((char *)th, th->doff << 2, skb->csum));
  16. }
  17. }
  1. unsigned csum_partial(const unsigned char *buff, unsigned len, unsigned sum)
  2. {
  3. return add32_with_carry(do_csum(buff, len), sum);
  4. }
  5. static inline unsigned add32_with_carry(unsigned a, unsigned b)
  6. {
  7. asm("addl %2, %0\n\t"
  8. "adcl $0, %0"
  9. : "=r" (a)
  10. : "0" (a), "r" (b));
  11. return a;
  12. }

do_csum()用于计算一段内存的校验和,这里用于累加TCP报头。

具体计算时用到一些技巧:

1. 反码累加时,按16位、32位、64位来累加的效果是一样的。

2. 使用内存对齐,减少内存操作的次数。

  1. static __force_inline unsigned do_csum(const unsigned char *buff, unsigned len)
  2. {
  3. unsigned odd, count;
  4. unsigned long result = 0;
  5. if (unlikely(len == 0))
  6. return result;
  7. /* 使起始地址为XXX0,接下来可按2字节对齐 */
  8. odd = 1 & (unsigned long) buff;
  9. if (unlikely(odd)) {
  10. result = *buff << 8; /* 因为机器是小端的 */
  11. len--;
  12. buff++;
  13. }
  14. count = len >> 1; /* nr of 16-bit words,这里可能余下1字节未算,最后会处理*/
  15. if (count) {
  16. /* 使起始地址为XX00,接下来可按4字节对齐 */
  17. if (2 & (unsigned long) buff) {
  18. result += *(unsigned short *)buff;
  19. count--;
  20. len -= 2;
  21. buff += 2;
  22. }
  23. count >>= 1; /* nr of 32-bit words,这里可能余下2字节未算,最后会处理 */
  24. if (count) {
  25. unsigned long zero;
  26. unsigned count64;
  27. /* 使起始地址为X000,接下来可按8字节对齐 */
  28. if (4 & (unsigned long)buff) {
  29. result += *(unsigned int *)buff;
  30. count--;
  31. len -= 4;
  32. buff += 4;
  33. }
  34. count >>= 1; /* nr of 64-bit words,这里可能余下4字节未算,最后会处理*/
  35. /* main loop using 64byte blocks */
  36. zero = 0;
  37. count64 = count >> 3; /* 64字节的块数,这里可能余下56字节未算,最后会处理 */
  38. while (count64) { /* 反码累加所有的64字节块 */
  39. asm ("addq 0*8(%[src]), %[res]\n\t"    /* b、w、l、q分别对应8、16、32、64位操作 */
  40. "addq 1*8(%[src]), %[res]\n\t"    /* [src]为指定寄存器的别名,效果应该等同于0、1等 */
  41. "adcq 2*8(%[src]), %[res]\n\t"
  42. "adcq 3*8(%[src]), %[res]\n\t"
  43. "adcq 4*8(%[src]), %[res]\n\t"
  44. "adcq 5*8(%[src]), %[res]\n\t"
  45. "adcq 6*8(%[src]), %[res]\n\t"
  46. "adcq 7*8(%[src]), %[res]\n\t"
  47. "adcq %[zero], %[res]"
  48. : [res] "=r" (result)
  49. : [src] "r" (buff), [zero] "r" (zero), "[res]" (result));
  50. buff += 64;
  51. count64--;
  52. }
  53. /* 从这里开始,反序处理之前可能漏算的字节 */
  54. /* last upto 7 8byte blocks,前面按8个8字节做计算单位,所以最多可能剩下7个8字节 */
  55. count %= 8;
  56. while (count) {
  57. asm ("addq %1, %0\n\t"
  58. "adcq %2, %0\n"
  59. : "=r" (result)
  60. : "m" (*(unsigned long *)buff), "r" (zero), "0" (result));
  61. --count;
  62. buff += 8;
  63. }
  64. /* 带进位累加result的高32位和低32位 */
  65. result = add32_with_carry(result>>32, result&0xffffffff);
  66. /* 之前始按8字节对齐,可能有4字节剩下 */
  67. if (len & 4) {
  68. result += *(unsigned int *) buff;
  69. buff += 4;
  70. }
  71. }
  72. /* 更早前按4字节对齐,可能有2字节剩下 */
  73. if (len & 2) {
  74. result += *(unsigned short *) buff;
  75. buff += 2;
  76. }
  77. }
  78. /* 最早之前按2字节对齐,可能有1字节剩下 */
  79. if (len & 1)
  80. result += *buff;
  81. /* 再次带进位累加result的高32位和低32位 */
  82. result = add32_with_carry(result>>32, result & 0xffffffff);
  83. /* 这里涉及到一个技巧,用于处理初始地址为奇数的情况 */
  84. if (unlikely(odd)) {
  85. result = from32to16(result); /* 累加到result的低16位 */
  86. /* result为:0 0 a b
  87. * 然后交换a和b,result变为:0 0 b a
  88. */
  89. result = ((result >> 8) & 0xff) | ((result & oxff) << 8);
  90. }
  91. return result; /* 返回result的低32位 */
  92. }
  1. static inline unsigned short from32to16(unsigned a)
  2. {
  3. unsigned short b = a >> 16;
  4. asm ("addw %w2, %w0\n\t"
  5. "adcw $0, %w0\n"
  6. : "=r" (b)
  7. : "0" (b), "r" (a));
  8. return b;
  9. }

csum_partial_copy_from_user()用于拷贝用户空间数据到内核空间,同时计算用户数据的校验和,

结果保存到skb->csum中(X86_64)。

  1. /**
  2. * csum_partial_copy_from_user - Copy and checksum from user space.
  3. * @src: source address (user space)
  4. * @dst: destination address
  5. * @len: number of bytes to be copied.
  6. * @isum: initial sum that is added into the result (32bit unfolded)
  7. * @errp: set to -EFAULT for an bad source address.
  8. *
  9. * Returns an 32bit unfolded checksum of the buffer.
  10. * src and dst are best aligned to 64bits.
  11. */
  12. unsigned int csum_partial_copy_from_user(const unsigned char __user *src,
  13. unsigned char *dst, int len, unsigned int isum, int *errp)
  14. {
  15. might_sleep();
  16. *errp = 0;
  17. if (likely(access_ok(VERIFY_READ, src, len))) {
  18. /* Why 6, not 7? To handle odd addresses aligned we would need to do considerable
  19. * complications to fix the checksum which is defined as an 16bit accumulator. The fix
  20. * alignment code is primarily for performance compatibility with 32bit and that will handle
  21. * odd addresses slowly too.
  22. * 处理X010、X100、X110的起始地址。不处理X001,因为这会使复杂度大增加。
  23. */
  24. if (unlikely((unsigned long)src & 6)) {
  25. while (((unsigned long)src & 6) && len >= 2) {
  26. __u16 val16;
  27. *errp = __get_user(val16, (__u16 __user *)src);
  28. if (*errp)
  29. return isum;
  30. *(__u16 *)dst = val16;
  31. isum = add32_with_carry(isum, val16);
  32. src += 2;
  33. dst += 2;
  34. len -= 2;
  35. }
  36. }
  37. /* 计算函数是用纯汇编实现的,应该是因为效率吧 */
  38. isum = csum_parial_copy_generic((__force void *)src, dst, len, isum, errp, NULL);
  39. if (likely(*errp == 0))
  40. return isum; /* 成功 */
  41. }
  42. *errp = -EFAULT;
  43. memset(dst, 0, len);
  44. return isum;
  45. }

上述的实现比较复杂,来看下最简单的csum_partial_copy_from_user()实现(um)。

  1. unsigned int csum_partial_copy_from_user(const unsigned char *src,
  2. unsigned char *dst, int len, int sum,
  3. int *err_ptr)
  4. {
  5. if (copy_from_user(dst, src, len)) { /* 拷贝用户空间数据到内核空间 */
  6. *err_ptr = -EFAULT; /* bad address */
  7. return (-1);
  8. }
  9. return csum_partial(dst, len, sum); /* 计算用户数据的校验和,会存到skb->csum中 */
  10. }

接收校验

@tcp_v4_rcv

/* 检查校验和 */

if (skb->ip_summed != CHECKSUM_UNNECESSARY && tcp_v4_checksum_init(skb))

goto bad_packet;

接收校验的第一部分,主要是计算伪首部。

  1. static int tcp_v4_checksum_init(struct sk_buff *skb)
  2. {
  3. /* 如果TCP报头、TCP数据的反码累加已经由硬件完成 */
  4. if (skb->ip_summed == CHECKSUM_HW) {
  5. /* 现在只需要再累加上伪首部,取反获取最终的校验和。
  6. * 校验和为0时,表示TCP数据报正确。
  7. */
  8. if (! tcp_v4_check(skb->h.th, skb->len, skb->nh.iph->saddr, skb->nh.iph->daddr, skb->csum)) {
  9. skb->ip_summed = CHECKSUM_UNNECESSARY;
  10. return 0; /* 校验成功 */
  11. } /* 没有else失败退出吗?*/
  12. }
  13. /* 对伪首部进行反码累加,主要用于软件方法 */
  14. skb->csum = csum_tcpudp_nofold(skb->nh.iph->saddr, skb->nh.iph->daddr, skb->len, IPPROTO_TCP, 0);
  15. /* 对于长度小于76字节的小包,接着累加TCP报头和报文,完成校验;否则,以后再完成检验。*/
  16. if (skb->len <= 76) {
  17. return __skb_checksum_complete(skb);
  18. }
  19. }

接收校验的第二部分,计算报头和报文。

tcp_v4_rcv、tcp_v4_do_rcv()

| --> tcp_checksum_complete()

| --> __tcp_checksum_complete()

| --> __skb_checksum_complete()

tcp_rcv_established()

| --> tcp_checksum_complete_user()

| --> __tcp_checksum_complete_user()

| --> __tcp_checksum_complete()

| --> __skb_checksum_complete()

  1. unsigned int __skb_checksum_complete(struct sk_buff *skb)
  2. {
  3. unsigned int sum;
  4. sum = (u16) csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));
  5. if (likely(!sum)) { /* sum为0表示成功了 */
  6. /* 硬件检测失败,软件检测成功了,说明硬件检测有误 */
  7. if (unlikely(skb->ip_summed == CHECKSUM_HW))
  8. netdev_rx_csum_fault(skb->dev);
  9. skb->ip_summed = CHECKSUM_UNNECESSARY;
  10. }
  11. return sum;
  12. }

计算skb包的校验和时,可以指定相对于skb->data的偏移量offset。

由于skb包可能由分页和分段,所以需要考虑skb->data + offset是位于此skb段的线性区中、

还是此skb的分页中,或者位于其它分段中。这个函数逻辑比较复杂。

  1. /* Checksum skb data. */
  2. unsigned int skb_checksum(const struct sk_buff *skb, int offset, int len, unsigned int csum)
  3. {
  4. int start = skb_headlen(skb); /* 线性区域长度 */
  5. /* copy > 0,说明offset在线性区域中。
  6. * copy < 0,说明offset在此skb的分页数据中,或者在其它分段skb中。
  7. */
  8. int i, copy = start - offset;
  9. int pos = 0; /* 表示校验了多少数据 */
  10. /* Checksum header. */
  11. if (copy > 0) { /* 说明offset在本skb的线性区域中 */
  12. if (copy > len)
  13. copy = len; /* 不能超过指定的校验长度 */
  14. /* 累加copy长度的线性区校验 */
  15. csum = csum_partial(skb->data + offset, copy, csum);
  16. if ((len -= copy) == 0)
  17. return csum;
  18. offset += copy; /* 接下来从这里继续处理 */
  19. pos = copy; /* 已处理数据长 */
  20. }
  21. /* 累加本skb分页数据的校验和 */
  22. for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
  23. int end;
  24. BUG_TRAP(start <= offset + len);
  25. end = start + skb_shinfo(skb)->frags[i].size;
  26. if ((copy = end - offset) > 0) { /* 如果offset位于本页中,或者线性区中 */
  27. unsigned int csum2;
  28. u8 *vaddr; /* 8位够吗?*/
  29. skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
  30. if (copy > len)
  31. copy = len;
  32. vaddr = kmap_skb_frag(frag); /* 把物理页映射到内核空间 */
  33. csum2 = csum_partial(vaddr + frag->page_offset + offset - start, copy, 0);
  34. kunmap_skb_frag(vaddr); /* 解除映射 */
  35. /* 如果pos为奇数,需要对csum2进行处理。
  36. * csum2:a, b, c, d => b, a, d, c
  37. */
  38. csum = csum_block_add(csum, csum2, pos);
  39. if (! (len -= copy))
  40. return csum;
  41. offset += copy;
  42. pos += copy;
  43. }
  44. start = end; /* 接下来从这里处理 */
  45. }
  46. /* 如果此skb是个大包,还有其它分段 */
  47. if (skb_shinfo(skb)->frag_list) {
  48. struct sk_buff *list = skb_shinfo(skb)->frag_list;
  49. for (; list; list = list->next) {
  50. int end;
  51. BUG_TRAP(start <= offset + len);
  52. end = start + list->len;
  53. if ((copy = end - offset) > 0) { /* 如果offset位于此skb分段中,或者分页,或者线性区 */
  54. unsigned int csum2;
  55. if (copy > len)
  56. copy = len;
  57. csum2 = skb_checksum(list, offset - start, copy, 0); /* 递归调用 */
  58. csum = csum_block_add(csum, csum2, pos);
  59. if ((len -= copy) == 0)
  60. return csum;
  61. offset += copy;
  62. pos += copy;
  63. }
  64. start = end;
  65. }
  66. }
  67. BUG_ON(len);
  68. return csum;
  69. }

TCP校验和的原理和实现的更多相关文章

  1. TCP传输工作原理

    引言 在TCP/IP体系结构中,IP协议只管将数据包尽力传送到目的主机,无论数据传输正确与否,它都不做验证,不发确认,也不保证数据包的顺序,因而不具有可靠性.这一问题要由传输层TCP协议来解决,TCP ...

  2. TCP/IP协议原理与应用笔记18:构成子网和超网

    1. 引言: (1)类别IP编址(Classful IP)的缺陷 • 固定的3种IP网络规模      C类地址:少于255台主机的网络      B类地址:介于255~65535台主机的网络     ...

  3. TCP/IP协议原理与应用笔记17:IP编址(重点)

    1. IP地址(通用标识符) 对于同一个网络设备(主机或路由器)的不同网络连接,需要不同的IP地址进行标识 2. 主机标识符 主要有下面三种方式的主机标识方式: (1)Name:是什么,可读性强(了解 ...

  4. TCP/IP协议原理学习笔记

    昨天学习了杨宁老师的TCP/IP协议原理第一讲和第二讲,主要介绍了OSI模型,整理如下: OSI是open system innerconnection的简称,即开放式系统互联参考模型,它把网络协议从 ...

  5. TCP协议设计原理

    TCP协议设计原理 最近去了解TCP协议,发现这是一个特别值得深思的协议.在本篇博客中,不会长篇大论的给大家介绍TCP协议特点.包头格式以及TCP的连接和断开等基本原理,而是会带大家深入理解为什么要这 ...

  6. TCP回话劫持原理和利用

    由于 TCP 协议并没有对 TCP 的传输包进行身份验证,所以在我们知道一个 TCP 连接中的 seq 和 ack 的信息后就可以很容易的伪造传输包,假装任意一方与另一方进行通信,我们将这一过程称为 ...

  7. tcp校验和

    伪首部(pseudo header),通常指TCP伪首部和UDP伪首部 TCP的校验和是必需的,而UDP的校验和是可选的 TCP校验是需要校验包头和数据的 //共12字节 typedef struct ...

  8. TCP/IP协议原理【转载】

    前述        各种L2数据网具有不同的通信协议与帧结构,其网络节点设备可以是各种类型的数据交换机(X.25.FR.Ethernet和ATM等分组交换机):而L3数据网(IP网或internet) ...

  9. TCP/IP协议原理与应用笔记01:OSI网络参考模型

    1.OSI参考模型 第7层应用层:直接对应用程序提供服务,应用程序可以变化,但要包括电子消息传输   第6层表示层:格式化数据,以便为应用程序提供通用接口.这可以包括加密服务   第5层会话层:在两个 ...

随机推荐

  1. 【转】MySQL的Replace into 与Insert into on duplicate key update真正的不同之处

    原文链接:http://www.jb51.net/article/47090.htm   今天听同事介绍oracle到mysql的数据migration,他用了Insert into ..... on ...

  2. .Net中的并行编程-4.实现高性能异步队列

    上文<.Net中的并行编程-3.ConcurrentQueue实现与分析>分析了ConcurrentQueue的实现,本章就基于ConcurrentQueue实现一个高性能的异步队列,该队 ...

  3. Nginx学习随笔

    题外话 第一份工作中项目中有DBA和运维,所以平时也只关注开发部分,对数据库和服务器关注比较少,记得那时有用户反馈网站很慢,老大让我联系运维看看是不是服务器的问题,那时也不知道Nginx是个什么东西. ...

  4. jQuery cxSlide 焦点图轮换

    cxSlide 是一个简单易用的焦点图展示插件,支持水平.纵向切换,透明过渡切换. 已支持 CSS 动画过渡切换.通过 CSS 动画切换,可以展示更多效果. 版本: jQuery v1.7+ jQue ...

  5. Junit单元测试笔记

    什么是单元测试? 单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的.很明确的功能是否正确. 单元测试目的? 执行单元测试,是为了证明某段代码的行为确实和开发者所期望的一致. 白盒测试和单 ...

  6. SAP中禁止特定用户更改密码

    在SAP管理中,有时一些账号因为是提供给大家作查询用的,受密码强度策略限制,密码不能为空.故密码设为通用后在公司内发布,为避免有些用户更改后造成其他用户无法登陆,我们可在使用TC-SU01,在登录数据 ...

  7. 劳动节脑洞大开!利用Debug API 获取 加壳客户端的MD5值

    系统 : Windows xp 程序 : 某游戏客户端 程序下载地址 :不提供 要求 : 远程注入 & 获取MD5值 使用工具 : vc++6.0 & OD 案例说明: 该游戏客户端对 ...

  8. 什么是REST?

    云计算的时代越来越多的提到一个词REST,那么什么是REST?如果你还不清楚,可以看这个视频和系列教程: http://www.restapitutorial.com/lessons/whatisre ...

  9. Xcode8如何去除控制台多余的打印信息

    Xcode8如何去除控制台多余的打印信息 最近刚使用了Xcode8.遇到了一些问题,总结如下.希望对大家有所帮助. 一.如何去除控制台多余的打印信息. 方法:点击Product----Scheme-- ...

  10. win环境变量立即生效

    执行 set PATH=C: 新打开的cmd窗口就立即生效了.