มีข้อมูลโค้ด C ที่คำนวณการเพิ่มความปลอดภัยมากเกินไปอย่างมีประสิทธิภาพโดยไม่ใช้คอมไพเลอร์ builtins หรือไม่


11

นี่คือฟังก์ชั่น C ที่เพิ่มintอีกอันหนึ่งซึ่งล้มเหลวหากเกิดโอเวอร์โฟลว์:

int safe_add(int *value, int delta) {
        if (*value >= 0) {
                if (delta > INT_MAX - *value) {
                        return -1;
                }
        } else {
                if (delta < INT_MIN - *value) {
                        return -1;
                }
        }

        *value += delta;
        return 0;
}

น่าเสียดายที่GCC หรือ Clang นั้นไม่ได้รับการปรับปรุงอย่างดี :

safe_add(int*, int):
        movl    (%rdi), %eax
        testl   %eax, %eax
        js      .L2
        movl    $2147483647, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jl      .L6
.L4:
        addl    %esi, %eax
        movl    %eax, (%rdi)
        xorl    %eax, %eax
        ret
.L2:
        movl    $-2147483648, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jle     .L4
.L6:
        movl    $-1, %eax
        ret

รุ่นนี้ด้วย __builtin_add_overflow()

int safe_add(int *value, int delta) {
        int result;
        if (__builtin_add_overflow(*value, delta, &result)) {
                return -1;
        } else {
                *value = result;
                return 0;
        }
}

ได้รับการปรับปรุงให้ดีขึ้น :

safe_add(int*, int):
        xorl    %eax, %eax
        addl    (%rdi), %esi
        seto    %al
        jo      .L5
        movl    %esi, (%rdi)
        ret
.L5:
        movl    $-1, %eax
        ret

แต่ฉันอยากรู้ว่ามีวิธีการไม่ใช้ builtins ที่จะได้รับการจับคู่รูปแบบโดย GCC หรือ Clang


1
ฉันเห็นว่ามีgcc.gnu.org/bugzilla/show_bug.cgi?id=48580ในบริบทของการคูณ แต่การเพิ่มรูปแบบควรจะง่ายกว่ามาก ฉันจะรายงานเรื่องนี้
Tavian Barnes

คำตอบ:


6

สิ่งที่ดีที่สุดที่ฉันได้รับหากคุณไม่สามารถเข้าถึงธงล้นของสถาปัตยกรรมคือการทำสิ่งต่างunsignedๆ แค่คิดเลขคณิตทุกบิตตรงนี้เราแค่สนใจบิตที่สูงที่สุดซึ่งเป็นบิตเครื่องหมายเมื่อตีความว่าเป็นค่าที่เซ็นชื่อ

(ทุกข้อผิดพลาดที่โมดูโล่ลงชื่อฉันไม่ได้ตรวจสอบเรื่องนี้อย่างเด็ดขาด แต่ฉันหวังว่าความคิดนั้นชัดเจน)

#include <stdbool.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;

  if (!ret) a[0] += b;
  return ret;
}

หากคุณพบรุ่นของการเพิ่มที่ไม่มี UB เช่นอะตอมมิกแอสเซมเบลอร์จะไม่มีสาขา (แต่มีคำนำหน้าล็อก)

#include <stdbool.h>
#include <stdatomic.h>
bool overadd(_Atomic(int) a[static 1], int b) {
  unsigned A = a[0];
  atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;
  return ret;
}

ดังนั้นหากเรามีการดำเนินการดังกล่าว แต่ยิ่ง“ ผ่อนคลาย” มากขึ้นก็สามารถปรับปรุงสถานการณ์ให้ดียิ่งขึ้นไปอีก

Take3:ถ้าเราใช้ "cast" พิเศษจากผลลัพธ์ที่ไม่ได้ลงชื่อกับผลลัพธ์ที่ลงชื่อแล้วตอนนี้ไม่มีสาขา:

#include <stdbool.h>
#include <stdatomic.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  //atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  unsigned res = (AeB & AuAB);
  signed N = M-1;
  N = -N - 1;
  a[0] =  ((AB > M) ? -(int)(-AB) : ((AB != M) ? (int)AB : N));
  return res > M;
}

2
ไม่ใช่ DV แต่ฉันเชื่อว่า XOR ตัวที่สองไม่ควรถูกปฏิเสธ ดูเช่นความพยายามนี้เพื่อทดสอบข้อเสนอทั้งหมด
Bob__

ฉันลองอะไรแบบนี้ แต่ไม่สามารถใช้งานได้ ดูมีแนวโน้ม แต่ฉันหวังว่า GCC จะปรับปรุงรหัสที่เป็นไปได้
. GitHub หยุดช่วยน้ำแข็ง

1
@PSkocik unsignedไม่มีนี้ไม่ได้ขึ้นอยู่กับการแสดงเครื่องหมายการคำนวณจะทำทั้งหมดเป็น แต่มันก็ขึ้นอยู่กับความจริงที่ว่าประเภทที่ไม่ได้ลงชื่อนั้นไม่ได้เป็นเพียงแค่สัญญาณบิตหลอกลวงเท่านั้น (ตอนนี้ทั้งสองได้รับการรับรองใน C2x นั่นคือค้างไว้สำหรับส่วนโค้งทั้งหมดที่เราสามารถหาได้) จากนั้นคุณไม่สามารถส่งunsignedผลลัพธ์กลับมาได้หากมีค่ามากกว่าINT_MAXนั้นนั่นคือการดำเนินการที่กำหนดไว้และอาจเพิ่มสัญญาณ
Jens Gustedt

1
@PSkocik ไม่น่าเสียดายที่ดูเหมือนจะ revolutianary ต่อคณะกรรมการ แต่นี่คือ "Take3" ที่เกิดขึ้นจริงโดยไม่มีกิ่งบนเครื่องของฉัน
Jens Gustedt

1
ขออภัยที่รบกวนคุณอีกครั้ง แต่ฉันคิดว่าคุณควรเปลี่ยน Take3 เป็นอย่างนี้เพื่อให้ได้ผลลัพธ์ที่ถูกต้อง ดูเหมือนว่ามีแนวโน้มแม้ว่า
Bob__

2

สถานการณ์ที่มีการดำเนินการที่ลงนามนั้นแย่กว่าที่ไม่ได้ลงนามมากและฉันเห็นเพียงรูปแบบเดียวสำหรับการเพิ่มที่ลงชื่อแล้วเฉพาะสำหรับเสียงดังกราวและเฉพาะเมื่อมีประเภทที่กว้างกว่า:

int safe_add(int *value, int delta)
{
    long long result = (long long)*value + delta;

    if (result > INT_MAX || result < INT_MIN) {
        return -1;
    } else {
        *value = result;
        return 0;
    }
}

เสียงดังกราวให้เหมือนกันกับasm __builtin_add_overflow:

safe_add:                               # @safe_add
        addl    (%rdi), %esi
        movl    $-1, %eax
        jo      .LBB1_2
        movl    %esi, (%rdi)
        xorl    %eax, %eax
.LBB1_2:
        retq

มิฉะนั้นทางออกที่ง่ายที่สุดที่ฉันคิดได้คือ (โดยใช้อินเตอร์เฟสเป็น Jens ที่ใช้):

_Bool overadd(int a[static 1], int b)
{
    // compute the unsigned sum
    unsigned u = (unsigned)a[0] + b;

    // convert it to signed
    int sum = u <= -1u / 2 ? (int)u : -1 - (int)(-1 - u);

    // see if it overflowed or not
    _Bool overflowed = (b > 0) != (sum > a[0]);

    // return the results
    a[0] = sum;
    return overflowed;
}

GCC และเสียงดังกราวสร้างคล้ายกันมากasm gcc ให้สิ่งนี้:

overadd:
        movl    (%rdi), %ecx
        testl   %esi, %esi
        setg    %al
        leal    (%rcx,%rsi), %edx
        cmpl    %edx, %ecx
        movl    %edx, (%rdi)
        setl    %dl
        xorl    %edx, %eax
        ret

เราต้องการคำนวณผลรวมunsignedดังนั้นunsignedจะต้องสามารถแสดงค่าทั้งหมดintโดยไม่ต้องผสานเข้าด้วยกัน ในการแปลงผลลัพธ์จากunsignedเป็นintง่าย ๆ ตรงกันข้ามจะมีประโยชน์เช่นกัน โดยรวมแล้วจะถือว่าส่วนประกอบสองอย่าง

บนแพลตฟอร์มยอดนิยมทั้งหมดที่ผมคิดว่าเราสามารถแปลงจากunsignedไปintโดยการโอนง่ายเหมือนint sum = u;แต่เป็น Jens กล่าวถึงแม้ตัวแปรล่าสุดของมาตรฐาน C2x ช่วยให้สามารถเพิ่มสัญญาณ วิธีที่เป็นธรรมชาติที่สุดต่อไปคือการทำสิ่งนั้น: *(unsigned *)&sum = u;แต่ตัวแปรที่ไม่ใช่กับดักของการแพ็ดดิ้งนั้นอาจแตกต่างกันสำหรับประเภทที่เซ็นชื่อและไม่ได้ลงนาม ตัวอย่างข้างต้นเป็นไปอย่างยากลำบาก โชคดีที่ทั้ง gcc และ clang ช่วยให้การแปลงนี้ยุ่งยาก

PS ตัวแปรสองตัวข้างต้นไม่สามารถเปรียบเทียบได้โดยตรงเนื่องจากมีพฤติกรรมที่แตกต่างกัน คำถามแรกติดตามคำถามเดิมและไม่ปิดบัง*valueในกรณีที่มีการล้น ส่วนที่สองตามหลังคำตอบจาก Jensและมักจะปิดบังตัวแปรที่ชี้ไปตามพารามิเตอร์ตัวแรก แต่มันก็ไม่แตกกิ่งก้านสาขา


คุณสามารถแสดง asm ที่สร้างขึ้นได้ไหม
. GitHub หยุดช่วยน้ำแข็ง

แทนที่ความเท่าเทียมกันโดย xor ในการตรวจสอบมากเกินไปเพื่อให้ได้ asm ที่ดีขึ้นด้วย gcc เพิ่ม asm
Alexander Cherepanov

1

รุ่นที่ดีที่สุดที่ฉันจะได้รับคือ:

int safe_add(int *value, int delta) {
    long long t = *value + (long long)delta;
    if (t != ((int)t))
        return -1;
    *value = (int) t;
    return 0;
}

ซึ่งผลิต:

safe_add(int*, int):
    movslq  %esi, %rax
    movslq  (%rdi), %rsi
    addq    %rax, %rsi
    movslq  %esi, %rax
    cmpq    %rsi, %rax
    jne     .L3
    movl    %eax, (%rdi)
    xorl    %eax, %eax
    ret
.L3:
    movl    $-1, %eax
    ret

ฉันประหลาดใจที่ไม่ได้ใช้ธงล้น ยังดีกว่าการตรวจสอบช่วงชัดเจนมาก แต่ก็ไม่ได้สรุปให้เพิ่มความยาว
Tavian Barnes

@TavianBarnes คุณพูดถูก แต่น่าเสียดายที่ไม่มีวิธีที่ดีในการใช้ธงล้นใน c (ยกเว้นคอมไพเลอร์เฉพาะของคอมไพเลอร์)
Iłya Bursov

1
รหัสนี้ทนทุกข์ทรมานจากการล้นล้นที่ลงนามซึ่งเป็นพฤติกรรมที่ไม่ได้กำหนด
emacs ผลักดันฉันถั่ว

@emacsdrivesmenuts คุณพูดถูกนักแสดงใน comparisson สามารถล้นได้
Jens Gustedt

@emacsdrivesmenuts นักแสดงไม่ได้กำหนด เมื่ออยู่นอกระยะintการส่งนักแสดงจากประเภทที่กว้างขึ้นจะสร้างมูลค่าที่กำหนดโดยการนำไปปฏิบัติหรือเพิ่มสัญญาณ การใช้งานทั้งหมดที่ฉันใส่ใจเกี่ยวกับการกำหนดเพื่อรักษารูปแบบบิตซึ่งทำสิ่งที่ถูกต้อง
Tavian Barnes

0

ฉันสามารถรับคอมไพเลอร์เพื่อใช้ธงสัญญาณโดยสมมติว่า (และยืนยัน) การแสดงส่วนประกอบสองอย่างโดยไม่ต้องเติมเต็มไบต์ การใช้งานดังกล่าวควรทำให้เกิดพฤติกรรมที่ต้องการในบรรทัดที่มีคำอธิบายประกอบโดยความคิดเห็นแม้ว่าฉันจะไม่สามารถหาการยืนยันที่เป็นทางการเชิงบวกของข้อกำหนดนี้ในมาตรฐาน (และอาจไม่มี)

โปรดทราบว่ารหัสต่อไปนี้จะจัดการกับการเพิ่มจำนวนเต็มบวกเท่านั้น แต่สามารถขยายได้

int safe_add(int* lhs, int rhs) {
    _Static_assert(-1 == ~0, "integers are not two's complement");
    _Static_assert(
        1u << (sizeof(int) * CHAR_BIT - 1) == (unsigned) INT_MIN,
        "integers have padding bytes"
    );
    unsigned value = *lhs;
    value += rhs;
    if ((int) value < 0) return -1; // impl. def., 6.3.1.3/3
    *lhs = value;
    return 0;
}

อัตราผลตอบแทนนี้ทั้งเสียงดังกราวและ GCC:

safe_add:
        add     esi, DWORD PTR [rdi]
        js      .L3
        mov     DWORD PTR [rdi], esi
        xor     eax, eax
        ret
.L3:
        mov     eax, -1
        ret

ฉันคิดว่านักแสดงในการเปรียบเทียบไม่ได้กำหนดไว้ แต่คุณสามารถหลีกเลี่ยงสิ่งนี้ได้เหมือนที่ฉันทำในคำตอบของฉัน แต่แล้วความสนุกทั้งหมดก็คือการสามารถครอบคลุมทุกกรณี การ_Static_assertให้บริการของคุณไม่ได้มีวัตถุประสงค์มากนักเพราะนี่เป็นเรื่องจริงในสถาปัตยกรรมปัจจุบันและจะถูกกำหนดไว้สำหรับ C2x
Jens Gustedt

2
@Jens จริง ๆ แล้วดูเหมือนว่านักแสดงจะถูกกำหนดตามการนำไปใช้ซึ่งไม่ได้กำหนดถ้าฉันอ่าน (ISO / IEC 9899: 2011) 6.3.1.3/3 อย่างถูกต้อง คุณตรวจสอบอีกครั้งได้ไหม (อย่างไรก็ตามการขยายสิ่งนี้ไปยังข้อโต้แย้งเชิงลบทำให้สิ่งทั้งหมดค่อนข้างซับซ้อนและในที่สุดก็คล้ายกับโซลูชันของคุณ)
Konrad Rudolph

คุณถูกต้องมันเป็น iplementation ที่กำหนด แต่อาจเพิ่มสัญญาณ :(
Jens Gustedt

@ Jens ใช่ฉันคิดว่าการใช้งานที่สมบูรณ์ของทั้งสองอาจยังคงมีช่องว่างภายใน INT_MAXบางทีรหัสควรทดสอบสำหรับการนี้โดยการเปรียบเทียบช่วงทฤษฎี ฉันจะแก้ไขโพสต์ แต่อีกครั้งฉันไม่คิดว่าควรใช้รหัสนี้ในทางปฏิบัติ
Konrad Rudolph
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.