Board logo

标题: [数值计算] 【已解决】批处理的整数变量是如何解析存储的? [打印本页]

作者: buyiyang    时间: 2023-9-24 16:16     标题: 【已解决】批处理的整数变量是如何解析存储的?

本帖最后由 buyiyang 于 2023-9-26 09:18 编辑

批处理数字精度是32位,如果是补码存储,为什么会出现这种情况?
  1. set /a n=-2147483648
  2. 无效数字。数字精确度限为 32 位。
复制代码
无法输入-2147483648,但却可以计算输出,
  1. set /a n=-2147483647-1
  2. set /a n=2147483647+1
复制代码
都可以为变量n赋值为-2147483648,问题是为什么set /a n=-2147483648不能实现赋值?
作者: hlzj88    时间: 2023-9-24 17:06

可以输入 负数,你的问题是第一个代码框里 使用了  /a ,这是令set计算的用法。
作者: 老刘1号    时间: 2023-9-24 17:23

纯猜想,可能parser实现的时候不管有没有负号都是按正数读的,之后才转成负数
作者: Five66    时间: 2023-9-24 19:16

大概是
cmd数值范围是-2147483647~214748364,cmd在解析时,判断-2147483648超出范围了,出错
但是32位有符号范围是: -2147483648~214748364,不影响计算,能正常保存并输出
0有两个的,,-2147483648其实是负0,但是一般只使用正0
作者: 老刘1号    时间: 2023-9-24 20:50

回复 4# Five66


    你这个结论非常不靠谱
从楼主的
  1. set /a n=2147483647+1
复制代码
测试样例可以看出来,INT_MAX+1是正常溢出到-2147483648也就是INT_MIN
32bit一共只能表示那么多数,哪有地方给你存两个零?
况且历史上也没有这种编码的先例
作者: Five66    时间: 2023-9-24 21:59

回复 5# 老刘1号


我是这样理解的
  1. 32位有符号正整数2147483647原码为:01111111 11111111 11111111 11111111
  2. 加1后
  3. 结果为:10000000  00000000  00000000  00000000 (没有溢出,还是32位,只是计算后最高位变了)
  4. 因为最高位为1,输出时被解析为负数
  5. 而32位有符号整数中
  6. -2147483648的原码就是:10000000  00000000  00000000  00000000  (表示负0)
  7. 正0是:00000000  00000000  00000000  00000000 (32个0)
复制代码

作者: buyiyang    时间: 2023-9-24 22:52

回复 6# Five66


    计算机算的是补码,10000000  00000000  00000000  00000000是-2147483648的补码而不是原码。
作者: buyiyang    时间: 2023-9-24 22:56

回复 3# 老刘1号

可能吧,我看了操作正数和负数的内存,似乎有点不同,但我不懂汇编。
set /a n=2147483647
  1. jae 00000067
  2. je 00000024
  3. das
  4. popad
  5. and [esi+3D],ch
  6. xor dh,[ecx]
  7. xor al,37
  8. xor al,38
  9. xor esi,[esi]
  10. xor al,37
复制代码
set /a n=-2147483647
  1. jae 00000067
  2. je 00000024
  3. das
  4. popad
  5. and [esi+3D],ch
  6. sub eax,37343132
  7. xor al,38
  8. xor esi,[esi]
  9. xor al,37
复制代码

作者: buyiyang    时间: 2023-9-24 23:01

回复 2# hlzj88


    set n=-2147483648确实可以,然后可以操作set /a n=n+1,但我想问的是set n=-2147483648为什么不行。
作者: hlzj88    时间: 2023-9-24 23:25

回复 9# buyiyang
哎,理解了一半题目就发言了,知道,没想到cmd有最大值的限定。六楼讲的应当是对的,也没时间和方法去求证。
作者: Five66    时间: 2023-9-24 23:32

回复 7# buyiyang


    说是说原码补码,其是我想说的是2进制值
作者: 老刘1号    时间: 2023-9-24 23:55

本帖最后由 老刘1号 于 2023-9-25 00:08 编辑

这种问题靠猜也没用
把我压箱底的NT4系统泄露源代码翻出来
  1. int SetWork(n)
  2. struct cmdnode *n ;
  3. {
  4.         TCHAR *tas ;    /* Tokenized argument string    */
  5.         TCHAR *wptr ;   /* Work pointer                 */
  6.         int i ;                 /* Work variable                */
  7.         //
  8.         // If extensions are enabled, things are different
  9.         //
  10.         if (fEnableExtensions) {
  11.             tas = n->argptr;
  12.             //
  13.             // Find first non-blank argument.
  14.             //
  15.             if (tas != NULL)
  16.             while (*tas && *tas <= SPACE)
  17.                 tas += 1;
  18.             //
  19.             // No arguments, same as old behavior.  Display current
  20.             // set of environment variables.
  21.             //
  22.             if (!tas || !*tas)
  23.                 return(DisplayEnv()) ;
  24.             //
  25.             // See if /A switch given.  If so, let arithmetic
  26.             // expression evaluator do the work.
  27.             //
  28.             if (!_tcsnicmp(tas, SetArithStr, 2))
  29.                 return SetArithWork(tas+2);
  30.             //
  31.             // See if first argument is quoted.  If so, strip off
  32.             // leading quote, spaces and trailing quote.
  33.             //
  34.             if (*tas == QUOTE) {
  35.                 tas += 1;
  36.                 while (*tas && *tas <= SPACE)
  37.                     tas += 1;
  38.                 wptr = _tcsrchr(tas, QUOTE);
  39.                 if (wptr)
  40.                     *wptr = NULLC;
  41.             }
  42.             //
  43.             // Find the equal sign in the argument.
  44.             //
  45.             wptr = _tcschr(tas, EQ);
  46.             //
  47.             // If no equal sign, then assume argument is variable name
  48.             // and user wants to see its value.  Display it.
  49.             //
  50.             if (!wptr)
  51.                 return DisplayEnvVariable(tas);
  52.             //
  53.             // Found the equal sign, so left of equal sign is variable name
  54.             // and right of equal sign is value.  Dont allow user to set
  55.             // a variable name that begins with an equal sign, since those
  56.             // are reserved for drive current directories.
  57.             //
  58.             *wptr++ = NULLC;
  59.             if (*wptr == EQ) {
  60.                 PutStdErr(MSG_BAD_SYNTAX, NOARGS);
  61.                 return(FAILURE) ;
  62.             }
  63.             return(SetEnvVar(tas, wptr, &CmdEnv)) ;
  64.         }
  65.         tas = TokStr(n->argptr, ONEQSTR, TS_WSPACE|TS_SDTOKENS) ;
  66.         if (!*tas)
  67.                 return(DisplayEnv()) ;
  68.         else {
  69.                 for (wptr = tas, i = 0 ; *wptr ; wptr += mystrlen(wptr)+1, i++)
  70.                         ;
  71.                 /* If too many parameters were given, the second parameter */
  72.                 /* wasn't an equal sign, or they didn't specify a string   */
  73.                 /* return an error message.                                */
  74.                 if ( i > 3 || *(wptr = tas+mystrlen(tas)+1) != EQ ||
  75.                     !mystrlen(mystrcpy(tas, stripit(tas))) ) {
  76. /* M013 */              PutStdErr(MSG_BAD_SYNTAX, NOARGS);
  77.                         return(FAILURE) ;
  78.                 } else {
  79.                         return(SetEnvVar(tas, wptr+2, &CmdEnv)) ;
  80.                 }
  81.         } ;
  82. }
复制代码
  1. /***    SetArithWork - set environment variable to value of arithmetic expression
  2. *
  3. *  Purpose:
  4. *      Set environment variable to value of arithmetic expression
  5. *
  6. *  int SetArithWork(TCHAR *tas)
  7. *
  8. *  Args:
  9. *      tas - pointer to null terminated string of the form:
  10. *
  11. *          VARNAME=expression
  12. *
  13. *  Returns:
  14. *      If valid expression, return SUCCESS otherwise FAILURE.
  15. *
  16. */
  17. int SetArithWork(TCHAR *tas)
  18. {
  19.     TCHAR c, szResult[ MAX_PATH ];
  20.     TCHAR *szOperator;
  21.     TCHAR *wptr;
  22.     DWORD i;
  23.     BOOLEAN bUnaryOpPossible;
  24.     int rc;
  25.     //
  26.     // If no input, declare an error
  27.     //
  28.     if (!tas || !tas) {
  29.         PutStdErr(MSG_BAD_SYNTAX, NOARGS);
  30.         return(FAILURE) ;
  31.     }
  32.     //
  33.     // Now evaluate the expression.  Syntax accepted:
  34.     //
  35.     //  <expr>:     '(' <expr> ')'
  36.     //            | <unary-op> <expr>
  37.     //            | <expr> <binary-op> <expr>
  38.     //            | <variable>
  39.     //            | <number>
  40.     //
  41.     //  <unary-op>:  '+' | '-' |
  42.     //               '~' | '!'
  43.     //
  44.     //  <binary-op>: '+' | '-' | '*' | '/' | '%'
  45.     //               '|' | '&' | '^' | '=' | ','
  46.     //
  47.     //  <number>:   C-syntax (e.g. 16 or 0x10)
  48.     //  <variable>: Any environment variable.  Dont need surround with % to get value
  49.     //
  50.     //  Operators have same meaning and precedence as ANSI C.  All arithmetic is
  51.     //  fixed, 32 bit arithmetic.  No floating point.
  52.     //
  53.     //
  54.     // Poor man's parser/evaluator with operand and operator stack.
  55.     //
  56.     iOperand = 0;
  57.     iOperator = 0;
  58.     bUnaryOpPossible = TRUE;
  59.     rc = SUCCESS;
  60.     do {
  61.         //
  62.         // Look at next non blank character
  63.         //
  64.         c = *tas;
  65.         if (c <= SPACE || c == QUOTE) {
  66.             if (*tas)
  67.                 tas += 1;
  68.         }
  69.         else
  70.         if (_istdigit(c)) {
  71.             //
  72.             // Digit, must be numeric operand.  Push it on operand stack
  73.             //
  74.             lOperands[ iOperand ].Value = _tcstol(tas, &tas, 0);
  75.             lOperands[ iOperand ].Name = NULL;
  76.             iOperand += 1;
  77.             if (_istdigit(*tas) || _istalpha(*tas)) {
  78.                 rc = MSG_SET_A_INVALID_NUMBER;
  79.                 break;
  80.             }
  81.             //
  82.             // Unary op not possible after a operand.
  83.             //
  84.             bUnaryOpPossible = FALSE;
  85.         }
  86.         else
  87.         if (bUnaryOpPossible && (szOperator = _tcschr(szUnaryOps, c))) {
  88.             //
  89.             // If unary op possible and we have one, then push it
  90.             // on the operator stack
  91.             tas += 1;
  92.             if (rc = DoArithOps( wUnaryOpCodes[szOperator - szUnaryOps] ))
  93.                 break;
  94.         }
  95.         else
  96.         if (!bUnaryOpPossible && (szOperator = _tcschr(szOps, c))) {
  97.             //
  98.             // If we have a binary op, push it on the operator stack
  99.             //
  100.             tas += 1;
  101.             if (c == L'<' || c == L'>') {
  102.                 if (*tas != c) {
  103.                     rc = MSG_SYNERR_GENL;
  104.                     break;
  105.                 }
  106.                 tas += 1;
  107.             }
  108.             if (*tas == EQ) {
  109.                 tas += 1;
  110.                 if (rc = DoArithOps( A_EQOP ))
  111.                     break;
  112.                 if (iOperand == 0) {
  113.                     rc = MSG_SET_A_MISSING_OPERAND;
  114.                     break;
  115.                 }
  116.                 lOperands[ iOperand ] = lOperands[ iOperand-1 ];
  117.                 iOperand += 1;
  118.             }
  119.             if (rc = DoArithOps( wOpCodes[szOperator - szOps] ))
  120.                 break;
  121.             //
  122.             // Unary op now possible.
  123.             //
  124.             if (c == RPOP) {
  125.                 bUnaryOpPossible = FALSE;
  126.             }
  127.             else {
  128.                 bUnaryOpPossible = TRUE;
  129.             }
  130.         }
  131.         else
  132.         if (!bUnaryOpPossible) {
  133.             rc = MSG_SET_A_MISSING_OPERATOR;
  134.             break;
  135.         }
  136.         else {
  137.             //
  138.             // Not a number or operator, must be a variable name.  The
  139.             // name must be terminated by a space or an operator.
  140.             //
  141.             wptr = tas;
  142.             while (*tas &&
  143.                    *tas > SPACE &&
  144.                    !_tcschr(szUnaryOps, *tas) &&
  145.                    !_tcschr(szOps, *tas)
  146.                   )
  147.                 tas += 1;
  148.             //
  149.             // If no variable or variable too long, bail
  150.             //
  151.             if (wptr == tas) {
  152.                 rc = MSG_SET_A_MISSING_OPERAND;
  153.                 break;
  154.             }
  155.             lOperands[ iOperand ].Value = 0;
  156.             lOperands[ iOperand ].Name = gmkstr((tas-wptr+1)*sizeof(TCHAR));
  157.             if (lOperands[ iOperand ].Name == NULL) {
  158.                 rc = MSG_NO_MEMORY;
  159.                 break;
  160.             }
  161.             //
  162.             // Have variable name.  Push name on operand stack with a zero
  163.             // value.  Value will be fetch when this operand is popped from
  164.             // operand stack.
  165.             //
  166.             _tcsncpy(lOperands[ iOperand ].Name, wptr, tas-wptr);
  167.             lOperands[ iOperand ].Name[tas-wptr] = NULLC;
  168.             iOperand += 1;
  169.             //
  170.             // Unary op not possible after a operand.
  171.             //
  172.             bUnaryOpPossible = FALSE;
  173.         }
  174.     } while (*tas);
  175.     if (rc == SUCCESS) {
  176.         //
  177.         // Do any pending operators.
  178.         //
  179.         rc = DoArithOps( 0 );
  180.         //
  181.         // If operator stack non-empty or more than one value
  182.         // on operand stack, then invalid expression
  183.         //
  184.         if (rc == SUCCESS && (iOperator || iOperand != 1)) {
  185.             if (iOperator)
  186.                 rc = MSG_SET_A_MISMATCHED_PARENS;
  187.             else
  188.                 rc = MSG_SET_A_MISSING_OPERAND;
  189.         }
  190.     }
  191.     if (rc != SUCCESS)
  192.         PutStdErr(rc, ONEARG, tas);
  193.     else {
  194.         //
  195.         // Valid result, display if not in a batch script.
  196.         //
  197.         if (!CurBat)
  198.             cmd_printf( TEXT("%d"), lOperands[ 0 ].Value ) ;
  199.     }
  200.     return rc;
  201. }
复制代码
  1. /***    DoArithOps - push operator on operator stack      arithmetic expression
  2. *
  3. *  Purpose:
  4. *      Push an operator on the operator stack.  Operator precedence handed here
  5. *
  6. *  int DoArithOps(USHORT OpCode)
  7. *
  8. *  Args:
  9. *      OpCode - operator value.  Precendence encoded in value, such that operators
  10. *          with higher precedence are numerically larger
  11. *
  12. *  Returns:
  13. *      If valid expression, return SUCCESS otherwise FAILURE.
  14. *
  15. */
  16. int
  17. DoArithOps(
  18.     USHORT OpCode
  19.     )
  20. {
  21.     USHORT op;
  22.     LONG op1, op2, result;
  23.     TCHAR szResult[ 32 ];
  24.     //
  25.     // Loop until we can push this operator onto the operator stack.
  26.     // These means we have to pop off and any operators on the stack
  27.     // that have a higher precedence than the new operator.
  28.     //
  29.     while (TRUE) {
  30.         if (OpCode == A_ENDOP) {
  31.             //
  32.             // If new opcode is a right parenthesis, then all done if
  33.             // the left parenthesis is on the top of the operator stack
  34.             //
  35.             if (iOperator != 0 && wOperators[iOperator-1] == A_BEGOP) {
  36.                 iOperator -= 1;
  37.                 OpCode = 0;
  38.                 break;
  39.             }
  40.             //
  41.             // No left paren left, keep popping until we see it. Error
  42.             // if nothing left to pop.
  43.             //
  44.             if (iOperator == 0)
  45.                 return MSG_SET_A_MISMATCHED_PARENS;
  46.         }
  47.         else
  48.         if (OpCode == 0) {
  49.             //
  50.             // OpCode zero is end of expression.  Pop everything off.
  51.             //
  52.             if (iOperator == 0)
  53.                 break;
  54.             }
  55.         else
  56.         if (iOperator == 0 || OpCode > wOperators[iOperator-1] || OpCode == A_BEGOP) {
  57.             //
  58.             // Done if no more operators to process or
  59.             // New operator has higher precedence than operator on top of stack
  60.             // or new operator is a left parenthesis
  61.             //
  62.             break;
  63.         }
  64.         //
  65.         // We need to pop and process (i.e. evaluate) the operator stack
  66.         // If it is a two operand stack, then get the second argument from
  67.         // the top of the operand stack.  Done if not two operands on the
  68.         // operand stack.
  69.         //
  70.         op = wOperators[--iOperator];
  71.         if (op < A_NEGOP) {
  72.             if (iOperand < 2)
  73.                 break;
  74.             op2 = PopOperand();
  75.         }
  76.         //
  77.         // Get the first operand from the top of the operand stack.  Error
  78.         // if not one there.
  79.         //
  80.         if (iOperand < 1)
  81.             break;
  82.         op1 = PopOperand();
  83.         //
  84.         // Evaluate the operator and push the result back on the operand stack
  85.         //
  86.         result = 0;
  87.         switch( op ) {
  88.             case A_LSHOP: result = op1 << op2;  break;
  89.             case A_RSHOP: result = op1 >> op2;  break;
  90.             case A_ADDOP: result = op1 + op2;   break;
  91.             case A_SUBOP: result = op1 - op2;   break;
  92.             case A_MULOP: result = op1 * op2;   break;
  93.             case A_DIVOP: if (op2 == 0) return MSG_SET_A_MISSING_OPERAND;
  94.                           result = op1 / op2;   break;
  95.             case A_MODOP: result = op1 % op2;   break;
  96.             case A_ANDOP: result = op1 & op2;   break;
  97.             case A_OROP:  result = op1 | op2;   break;
  98.             case A_XOROP: result = op1 ^ op2;   break;
  99.             case A_NEGOP: result = - op1;       break;
  100.             case A_POSOP: result = + op1;       break;
  101.             case A_NOTOP: result = ~ op1;       break;
  102.             case A_SEPOP: result = op2;         break;
  103.             case A_EQOP:  if (lOperands[iOperand].Name == NULL) return MSG_SET_A_BAD_ASSIGN;
  104.                           result = op2;
  105.                           //
  106.                           // Left handside has variable name, convert result
  107.                           // to text and store as value of variable.
  108.                           //
  109.                           _sntprintf( szResult, 32, TEXT("%d"), result ) ;
  110.                           if (SetEnvVar(lOperands[iOperand].Name, szResult, &CmdEnv) != SUCCESS)
  111.                               return GetLastError();
  112.                           break;
  113.             default:    break;
  114.         }
  115.         lOperands[iOperand].Value = result;
  116.         lOperands[iOperand].Name = NULL;
  117.         iOperand += 1;
  118.     }
  119.     //
  120.     // If new operator code is not the end of the expression, push it onto the
  121.     // operator stack.
  122.     //
  123.     if (OpCode != 0)
  124.         wOperators[ iOperator++ ] = OpCode;
  125.     return SUCCESS;
  126. }
复制代码

作者: Five66    时间: 2023-9-25 00:43

回复 12# 老刘1号


    厉害了,看了半天,/a开关的执行算术运算的操作数op1, op2跟返回值result的类型都是LONG,不过那是以前的LONG,大概是现在的int32?至于解析的那部分,完全看不懂
作者: Five66    时间: 2023-9-25 01:02

查了下,_tcstol函数是将字符串转 32 位长整数 (long)

而后面的

            if (_istdigit(*tas) || _istalpha(*tas)) {
                rc = MSG_SET_A_INVALID_NUMBER;
                break;
            }

应该就是是在解析操作数时进行判断数字是否合法的

再后面的
case A_ADDOP: result = op1 + op2;   break;

case A_SEPOP: result = op2;         break;
是直接用解析出来的操作数进行计算和赋值的
作者: 老刘1号    时间: 2023-9-25 01:30

回复 14# Five66


    哈哈,我猜对了
进行_tcstol的时候c已经是一个dight了,而c=*tas,那么也就是说tas指向的字符串是数字开头的,不存在负数的情况
下面计算函数中的单目运算符-也可以佐证我的观点
那么就很明确了,由于set的tokenize机制,-号被当做单目运算符而不是数字本身的一部分,而解析数字本身时由于正数的表示范围比负数少一个(0占了一个位置),所以绝对值最大的负数取相反数之后已经超出32位范围,所以转换失败;但这并不影响可以正常算出这个数字。
作者: 老刘1号    时间: 2023-9-25 01:41

补充一些细节,
  1. long wcstol( const wchar_t *nptr, wchar_t **endptr, intbase);
  2. 释义:将字符串类型根据不同的基转化数字形式。
  3. 其中:[in] nptr 表示要进行扫描字符串指针
  4. [out] endptr  存储扫描后无法转化的剩余的字符串
复制代码
也就是说,读取后剩余部分会覆盖掉原来的tas
下面if的意思是,假如读取后的剩余部分的第一个字符还是数字或者字母的话,那么失败
例子:1000a执行后还剩下a,所以解析失败。
而2147483648由于超出32bit正数范围,解析后tas会指向8,读进去的数字是214748364,由于现在指向的还是一个数字,if命中,命令报错。
作者: czjt1234    时间: 2023-9-25 08:09

不明觉厉
作者: buyiyang    时间: 2023-9-25 17:10

本帖最后由 buyiyang 于 2023-9-25 19:15 编辑

回复 12# 老刘1号


    大概明白了,算数运算的解析字符串表达式是逐字符的,-首先被解析,被解析为操作符了,然后后面的数字2147483648被解析为操作数,操作数字符串要转长整型,过程中由于溢出遇到错误tas会指向最后停止的位置,判断指向的是数字8就会报错。

    十分感谢@老刘1号提供的源码!基于第二个源码我再详细理解一下解析过程,有几个重要的变量,bUnaryOpPossible来判断是否允许一元操作符出现,初始值为TRUE。然后c=*tas,c是TCHAR类型的,c那么就是tas字符串数组的第一个字符,然后判断c是空白、字母、数字、操作符还是变量名,空白字符删除,操作符压入操作符栈,变量名压入操作数栈,变量名存储在 lOperands 数组中的Name字段,计算表达式时从环境变量中获取其值,如果是数字就从对tas字符串进行_tcstol处理转成长整型,遇到错误就将指针指向最后停止的位置,去除tas字符串前面成功转成长整型的部分。每判断一个字符,都会tas += 1,去除tas字符串中前一个已经判断过的字符。进行do while循环中直到尾tas字符串\0。

    比如,set /a n=1+1,对于字符串"n=11+1",首先指针指向n,c为'n',判断为变量名,然后指针移到=,判断为操作符,指针移到1,判断为数字,对"11+1"进行_tcstol处理,在+遇到错误,11转成长整型作为操作数,指针移到+,判断为操作符,指针移到1,判断为数字,对"1"进行_tcstol处理,1转成长整型作为操作数,指针移到\0,循环结束。
作者: buyiyang    时间: 2023-9-25 17:28

回复 15# 老刘1号
进行_tcstol的时候c已经是一个dight了,而c=*tas,那么也就是说tas指向的字符串是数字开头的

这个是如何看出来的?
作者: 老刘1号    时间: 2023-9-25 17:34

回复 19# buyiyang


    见第二个文件的69和75行
作者: buyiyang    时间: 2023-9-25 17:44

回复 20# 老刘1号


    这里从tas字符数组第一个地址开始判断每个字符是空白、字母、数字、操作符,如何得出tas指向的字符串是数字开头的?do循环中,c=*tas应该是tas字符数组的第一个字符,判断完后,然后tas += 1,再赋值c=*tas是tas字符数组的第二个字符。
作者: 老刘1号    时间: 2023-9-25 18:44

回复 21# buyiyang


   可能我表述不太清晰,你可以理解为tas指向的子串(原字符串去掉了前面若干字符剩余的部分)
因为c的字符串本质上就是指向以0结尾的字节数组的指针嘛,指针+1了指向的那个字符串就变化了(相当于最前面的字符就没了)。
作者: buyiyang    时间: 2023-9-25 19:10

回复 22# 老刘1号


    可能误解你的意思了,你的意思是:c=*tas,当能进行_tcstol的时候说明c是一个dight,此时tas指向的字符串是数字开头的。明白了,我以为你说一开始的tas




欢迎光临 批处理之家 (http://www.bathome.net/) Powered by Discuz! 7.2