gcc-10.0.1 Segfault เฉพาะ


23

ฉันมีแพ็คเกจ R ที่มีโค้ดที่คอมไพล์ C ซึ่งค่อนข้างเสถียรมาระยะหนึ่งแล้วและได้รับการทดสอบกับแพลตฟอร์มและคอมไพเลอร์ที่หลากหลาย (windows / osx / debian / fedora gcc / clang)

เมื่อเร็ว ๆ นี้มีการเพิ่มแพลตฟอร์มใหม่เพื่อทดสอบแพคเกจอีกครั้ง:

Logs from checks with gcc trunk aka 10.0.1 compiled from source
on Fedora 30. (For some archived packages, 10.0.0.)

x86_64 Fedora 30 Linux

FFLAGS="-g -O2 -mtune=native -Wall -fallow-argument-mismatch"
CFLAGS="-g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"
CXXFLAGS="-g -O2 -Wall -pedantic -mtune=native -Wno-ignored-attributes -Wno-deprecated-declarations -Wno-parentheses -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"

ณ จุดที่โค้ดที่คอมไพล์เริ่มต้นทันทีทำการแบ่งส่วนข้อมูลตามบรรทัดเหล่านี้:

 *** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'

ฉันสามารถสร้าง segfault อย่างสม่ำเสมอโดยใช้rocker/r-basecontainer docker พร้อมgcc-10.0.1กับระดับการปรับให้-O2เหมาะสม การใช้การเพิ่มประสิทธิภาพที่ต่ำกว่าจะกำจัดปัญหาได้ เรียกใช้การตั้งค่าอื่น ๆ รวมถึงภายใต้ valgrind (ทั้ง -O0 และ -O2), UBSAN (gcc / clang) ไม่แสดงปัญหาใด ๆ เลย ฉันก็แน่ใจด้วยว่านี่เป็นสิ่งที่เกิดขึ้นgcc-10.0.0แต่ไม่มีข้อมูล

ฉันใช้gcc-10.0.1 -O2เวอร์ชันด้วยgdbและสังเกตเห็นบางสิ่งที่ดูแปลกสำหรับฉัน:

gdb vs code

ในขณะที่ก้าวผ่านส่วนที่ไฮไลต์จะปรากฏการเริ่มต้นขององค์ประกอบที่สองของอาร์เรย์ถูกข้ามไป ( R_allocเป็นเสื้อคลุมรอบ ๆmallocขยะตัวเองที่รวบรวมเมื่อกลับการควบคุมไปที่ R; segfault เกิดขึ้นก่อนที่จะกลับไป R) หลังจากนั้นโปรแกรมจะทำงานล้มเหลวเมื่อมีการเข้าถึงองค์ประกอบที่ไม่ได้กำหนดค่าเริ่มต้น (ในรุ่น gcc.10.0.1 -O2)

ฉันแก้ไขสิ่งนี้โดยเริ่มต้นองค์ประกอบที่เป็นปัญหาอย่างชัดเจนทุกที่ในรหัสที่นำไปสู่การใช้งานองค์ประกอบ แต่จริงๆแล้วควรได้รับการเริ่มต้นเป็นสตริงว่างหรืออย่างน้อยนั่นคือสิ่งที่ฉันจะสันนิษฐาน

ฉันขาดอะไรที่ชัดเจนหรือทำอะไรโง่ ๆ ? ทั้งสองมีแนวโน้มที่สมเหตุสมผลเนื่องจาก C เป็นภาษาที่สองของฉันในตอนนี้ มันแปลกมากที่ตอนนี้ครอบตัดหมดแล้วและฉันไม่สามารถเข้าใจได้ว่าคอมไพเลอร์พยายามทำอะไร


UPDATE : คำแนะนำในการทำซ้ำนี้แม้ว่านี้จะทำซ้ำตราบdebian:testingภาชนะนักเทียบท่ามีที่gcc-10 gcc-10.0.1นอกจากนี้ไม่เพียงแค่เรียกใช้คำสั่งเหล่านี้ถ้าคุณไม่เชื่อฉัน

ขออภัยนี่ไม่ใช่ตัวอย่างที่ทำซ้ำได้ขั้นต่ำ

docker pull rocker/r-base
docker run --rm -ti --security-opt seccomp=unconfined \
  rocker/r-base /bin/bash
apt-get update
apt-get install gcc-10 gdb
gcc-10 --version  # confirm 10.0.1
# gcc-10 (Debian 10-20200222-1) 10.0.1 20200222 (experimental) 
# [master revision 01af7e0a0c2:487fe13f218:e99b18cf7101f205bfdd9f0f29ed51caaec52779]

mkdir ~/.R
touch ~/.R/Makevars
echo "CC = gcc-10
CFLAGS = -g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection
" >> ~/.R/Makevars

R -d gdb --vanilla

จากนั้นใน R คอนโซลหลังจากการพิมพ์runที่จะได้รับgdbการเรียกใช้โปรแกรม:

f.dl <- tempfile()
f.uz <- tempfile()

github.url <- 'https://github.com/brodieG/vetr/archive/v0.2.8.zip'

download.file(github.url, f.dl)
unzip(f.dl, exdir=f.uz)
install.packages(
  file.path(f.uz, 'vetr-0.2.8'), repos=NULL,
  INSTALL_opts="--install-tests", type='source'
)
# minimal set of commands to segfault
library(vetr)
alike(pairlist(a=1, b="character"), pairlist(a=1, b=letters))
alike(pairlist(1, "character"), pairlist(1, letters))
alike(NULL, 1:3)                  # not a wild card at top level
alike(list(NULL), list(1:3))      # but yes when nested
alike(list(NULL, NULL), list(list(list(1, 2, 3)), 1:25))
alike(list(NULL), list(1, 2))
alike(list(), list(1, 2))
alike(matrix(integer(), ncol=7), matrix(1:21, nrow=3))
alike(matrix(character(), nrow=3), matrix(1:21, nrow=3))
alike(
  matrix(integer(), ncol=3, dimnames=list(NULL, c("R", "G", "B"))),
  matrix(1:21, ncol=3, dimnames=list(NULL, c("R", "G", "B")))
)

# Adding tests from docs

mx.tpl <- matrix(
  integer(), ncol=3, dimnames=list(row.id=NULL, c("R", "G", "B"))
)
mx.cur <- matrix(
  sample(0:255, 12), ncol=3, dimnames=list(row.id=1:4, rgb=c("R", "G", "B"))
)
mx.cur2 <-
  matrix(sample(0:255, 12), ncol=3, dimnames=list(1:4, c("R", "G", "B")))

alike(mx.tpl, mx.cur2)

การตรวจสอบใน gdb แสดงให้เห็นอย่างรวดเร็ว (ถ้าฉันเข้าใจถูกต้อง) ว่า CSR_strmlen_xพยายามเข้าถึงสตริงที่ไม่ได้กำหนดค่าเริ่มต้น

UPDATE 2 : นี่เป็นฟังก์ชั่นแบบเรียกซ้ำสูงและยิ่งไปกว่านั้นบิตการเริ่มต้นสตริงได้รับการเรียกหลายครั้ง นี่คือ b / c ส่วนใหญ่ที่ฉันขี้เกียจเราต้องการเพียงสตริงเริ่มต้นสำหรับครั้งเดียวที่เราพบสิ่งที่เราต้องการรายงานในการสอบถามซ้ำ แต่มันง่ายกว่าที่จะเริ่มต้นทุกครั้งที่เป็นไปได้ที่จะพบบางสิ่งบางอย่าง ฉันพูดถึงสิ่งนี้เพราะสิ่งที่คุณจะเห็นต่อไปจะแสดงการเริ่มต้นหลายรายการ แต่มีเพียงอันเดียวเท่านั้น (น่าจะเป็นอันแรกที่มีที่อยู่ <0x1400000001>)

ฉันไม่สามารถรับประกันได้ว่าสิ่งที่ฉันแสดงที่นี่เกี่ยวข้องโดยตรงกับองค์ประกอบที่ทำให้เกิด segfault (แม้ว่าจะเป็นที่อยู่ที่ผิดกฎหมายเหมือนกัน) แต่เป็น @ nate-eldredge ถามว่ามันแสดงให้เห็นว่าองค์ประกอบอาร์เรย์ไม่ได้ เริ่มต้นอย่างใดอย่างหนึ่งก่อนที่จะกลับมาหรือเพียงแค่หลังจากกลับมาในฟังก์ชั่นการโทร โปรดทราบว่าฟังก์ชั่นการโทรกำลังเริ่มต้น 8 รายการเหล่านี้และฉันแสดงให้พวกเขาทั้งหมดโดยที่พวกเขาเต็มไปด้วยขยะหรือหน่วยความจำที่เข้าถึงไม่ได้

ป้อนคำอธิบายรูปภาพที่นี่

UPDATE 3 ปัญหาการถอดแยกชิ้นส่วนของคำถาม:

Breakpoint 1, ALIKEC_res_strings_init () at alike.c:75
75    return res;
(gdb) p res.current[0]
$1 = 0x7ffff46a0aa5 "%s%s%s%s"
(gdb) p res.current[1]
$2 = 0x1400000001 <error: Cannot access memory at address 0x1400000001>
(gdb) disas /m ALIKEC_res_strings_init
Dump of assembler code for function ALIKEC_res_strings_init:
53  struct ALIKEC_res_strings ALIKEC_res_strings_init() {
   0x00007ffff4687fc0 <+0>: endbr64 

54    struct ALIKEC_res_strings res;

55  
56    res.target = (const char **) R_alloc(5, sizeof(const char *));
   0x00007ffff4687fc4 <+4>: push   %r12
   0x00007ffff4687fc6 <+6>: mov    $0x8,%esi
   0x00007ffff4687fcb <+11>:    mov    %rdi,%r12
   0x00007ffff4687fce <+14>:    push   %rbx
   0x00007ffff4687fcf <+15>:    mov    $0x5,%edi
   0x00007ffff4687fd4 <+20>:    sub    $0x8,%rsp
   0x00007ffff4687fd8 <+24>:    callq  0x7ffff4687180 <R_alloc@plt>
   0x00007ffff4687fdd <+29>:    mov    $0x8,%esi
   0x00007ffff4687fe2 <+34>:    mov    $0x5,%edi
   0x00007ffff4687fe7 <+39>:    mov    %rax,%rbx

57    res.current = (const char **) R_alloc(5, sizeof(const char *));
   0x00007ffff4687fea <+42>:    callq  0x7ffff4687180 <R_alloc@plt>

58  
59    res.target[0] = "%s%s%s%s";
   0x00007ffff4687fef <+47>:    lea    0x1764a(%rip),%rdx        # 0x7ffff469f640
   0x00007ffff4687ff6 <+54>:    lea    0x18aa8(%rip),%rcx        # 0x7ffff46a0aa5
   0x00007ffff4687ffd <+61>:    mov    %rcx,(%rbx)

60    res.target[1] = "";

61    res.target[2] = "";
   0x00007ffff4688000 <+64>:    mov    %rdx,0x10(%rbx)

62    res.target[3] = "";
   0x00007ffff4688004 <+68>:    mov    %rdx,0x18(%rbx)

63    res.target[4] = "";
   0x00007ffff4688008 <+72>:    mov    %rdx,0x20(%rbx)

64  
65    res.tar_pre = "be";

66  
67    res.current[0] = "%s%s%s%s";
   0x00007ffff468800c <+76>:    mov    %rax,0x8(%r12)
   0x00007ffff4688011 <+81>:    mov    %rcx,(%rax)

68    res.current[1] = "";

69    res.current[2] = "";
   0x00007ffff4688014 <+84>:    mov    %rdx,0x10(%rax)

70    res.current[3] = "";
   0x00007ffff4688018 <+88>:    mov    %rdx,0x18(%rax)

71    res.current[4] = "";
   0x00007ffff468801c <+92>:    mov    %rdx,0x20(%rax)

72  
73    res.cur_pre = "is";

74  
75    return res;
=> 0x00007ffff4688020 <+96>:    lea    0x14fe0(%rip),%rax        # 0x7ffff469d007
   0x00007ffff4688027 <+103>:   mov    %rax,0x10(%r12)
   0x00007ffff468802c <+108>:   lea    0x14fcd(%rip),%rax        # 0x7ffff469d000
   0x00007ffff4688033 <+115>:   mov    %rbx,(%r12)
   0x00007ffff4688037 <+119>:   mov    %rax,0x18(%r12)
   0x00007ffff468803c <+124>:   add    $0x8,%rsp
   0x00007ffff4688040 <+128>:   pop    %rbx
   0x00007ffff4688041 <+129>:   mov    %r12,%rax
   0x00007ffff4688044 <+132>:   pop    %r12
   0x00007ffff4688046 <+134>:   retq   
   0x00007ffff4688047:  nopw   0x0(%rax,%rax,1)

End of assembler dump.

อัปเดต 4 :

ดังนั้นการพยายามวิเคราะห์มาตรฐานที่นี่คือส่วนต่าง ๆ ของมันที่ดูเหมือนเกี่ยวข้อง ( ฉบับร่าง C11 ):

6.3.2.3 การแปลง Par7> ตัวดำเนินการอื่น> พอยน์เตอร์

ตัวชี้ชนิดวัตถุอาจถูกแปลงเป็นตัวชี้ชนิดวัตถุอื่น หากตัวชี้ผลลัพธ์ถูกจัดแนวอย่างไม่ถูกต้อง 68) สำหรับประเภทที่อ้างอิงพฤติกรรมนั้นจะไม่ได้กำหนดไว้
มิฉะนั้นเมื่อแปลงกลับมาอีกครั้งผลลัพธ์จะเปรียบเทียบเท่ากับตัวชี้เดิม เมื่อตัวชี้ไปยังวัตถุถูกแปลงเป็นตัวชี้ไปยังชนิดของตัวอักษรผลลัพธ์จะชี้ไปที่จำนวนไบต์ต่ำสุดที่ระบุของวัตถุ การเพิ่มขึ้นอย่างต่อเนื่องของผลลัพธ์ขึ้นอยู่กับขนาดของวัตถุทำให้ตัวชี้ไปยังไบต์ที่เหลือของวัตถุ

6.5 นิพจน์ Par6

ประเภทที่มีประสิทธิภาพของวัตถุสำหรับการเข้าถึงค่าที่เก็บไว้เป็นประเภทของวัตถุที่ประกาศถ้ามี 87) หากค่าถูกเก็บไว้ในวัตถุที่ไม่มีประเภทประกาศผ่าน lvalue ที่มีประเภทที่ไม่ใช่ประเภทตัวอักษรแล้วประเภทของ lvalue จะกลายเป็นชนิดที่มีประสิทธิภาพของวัตถุสำหรับการเข้าถึงนั้นและการเข้าถึงต่อมาที่ไม่ได้ ปรับเปลี่ยนค่าที่เก็บไว้ หากค่าถูกคัดลอกไปยังวัตถุที่ไม่มีประเภทที่ประกาศโดยใช้ memcpy หรือ memmove หรือถูกคัดลอกเป็นอาร์เรย์ประเภทอักขระดังนั้นชนิดที่มีประสิทธิภาพของวัตถุที่ถูกปรับเปลี่ยนสำหรับการเข้าถึงนั้นและสำหรับการเข้าถึงในภายหลังที่ไม่ได้แก้ไขค่านั้นคือ ชนิดที่มีประสิทธิภาพของวัตถุซึ่งค่าถูกคัดลอกถ้ามี สำหรับการเข้าถึงวัตถุอื่นที่ไม่มีประเภทที่ประกาศไว้ชนิดของวัตถุที่มีประสิทธิภาพนั้นเป็นเพียงประเภทของ lvalue ที่ใช้สำหรับการเข้าถึง

87) วัตถุที่จัดสรรไม่มีชนิดที่ประกาศไว้

IIUC R_allocส่งคืนออฟเซ็ตเป็นmallocบล็อก ed ที่รับประกันว่าจะdoubleจัดตำแหน่งและขนาดของบล็อกหลังจากออฟเซ็ตมีขนาดที่ร้องขอ (นอกจากนี้ยังมีการจัดสรรก่อนออฟเซ็ตสำหรับข้อมูลเฉพาะ R) R_allocปลดเปลื้องตัวชี้ไป(char *)ที่กลับ

มาตรา 6.2.5 พาร์ 29

ตัวชี้ไปยังโมฆะจะต้องมีการเป็นตัวแทนและความต้องการการจัดตำแหน่งเช่นเดียวกับตัวชี้ไปยังประเภทตัวละคร 48) ในทำนองเดียวกันพอยน์เตอร์สำหรับประเภทที่เข้ากันได้หรือไม่ผ่านการรับรองของประเภทที่เข้ากันได้จะต้องมีการเป็นตัวแทนและความต้องการการจัดตำแหน่งเดียวกัน ตัวชี้ไปยังประเภทโครงสร้างทั้งหมดจะต้องมีข้อกำหนดการเป็นตัวแทนและการจัดตำแหน่งที่เหมือนกัน
พอยน์เตอร์ทั้งหมดไปยังประเภทยูเนี่ยนจะต้องมีข้อกำหนดการเป็นตัวแทนและการจัดตำแหน่งที่เหมือนกัน
ตัวชี้ไปยังประเภทอื่นไม่จำเป็นต้องมีข้อกำหนดการเป็นตัวแทนหรือการจัดตำแหน่งเดียวกัน

48) ข้อกำหนดการเป็นตัวแทนและการจัดตำแหน่งเดียวกันมีขึ้นเพื่อบอกนัยถึงการแทนกันของฟังก์ชันการคืนค่าจากฟังก์ชันและสมาชิกของสหภาพ

ดังนั้นคำถามคือ "เราได้รับอนุญาตให้หล่อ(char *)ไป(const char **)และเขียนถึงว่ามันเป็น(const char **)" การอ่านข้างต้นของฉันคือตราบใดที่พอยน์เตอร์ในระบบที่โค้ดทำงานนั้นมีการจัดตำแหน่งที่เข้ากันได้กับdoubleการจัดตำแหน่งจากนั้นก็ไม่เป็นไร

เราละเมิด "นามแฝงที่เข้มงวด" หรือไม่ เช่น:

6.5 พาร์ 7

วัตถุต้องมีค่าที่เก็บไว้เข้าถึงได้โดยนิพจน์ lvalue ที่มีประเภทใดประเภทหนึ่งต่อไปนี้: 88)

- ชนิดที่เข้ากันได้กับชนิดของวัตถุที่มีประสิทธิภาพ ...

88) ความตั้งใจของรายการนี้คือการระบุสถานการณ์เหล่านั้นซึ่งวัตถุอาจหรืออาจไม่ได้รับนามแฝง

ดังนั้นคอมไพเลอร์ควรคิดอย่างไรกับชนิดของวัตถุที่มีประสิทธิภาพซึ่งชี้โดยres.target(หรือres.current) คืออะไร? น่าจะเป็นประเภทที่ประกาศไว้(const char **)หรือเป็นจริงที่คลุมเครือ? ฉันรู้สึกว่ามันไม่ใช่ในกรณีนี้เท่านั้นเพราะไม่มี 'lvalue' อื่น ๆ ในขอบเขตที่เข้าถึงวัตถุเดียวกัน

ฉันจะยอมรับว่าฉันกำลังดิ้นรนอย่างยิ่งยวดที่จะดึงความรู้สึกจากส่วนเหล่านี้ของมาตรฐาน


หากยังไม่ได้ตรวจสอบมันอาจคุ้มค่าที่จะดูการถอดแยกชิ้นส่วนเพื่อดูว่ากำลังทำอะไรอยู่ และเพื่อเปรียบเทียบการถอดแยกระหว่างรุ่น gcc
kaylum

2
ฉันจะไม่พยายามยุ่งกับ GCC รุ่นลำตัว มันดีที่ได้สนุกกับมัน แต่มันถูกเรียกว่า trunk ด้วยเหตุผล น่าเสียดายที่เกือบจะเป็นไปไม่ได้ที่จะบอกว่าเกิดข้อผิดพลาดหากไม่มี (1) มีรหัสและการกำหนดค่าที่แน่นอน (2) มีรุ่น GCC เดียวกัน (3) ในสถาปัตยกรรมเดียวกัน ฉันขอแนะนำให้ตรวจสอบว่าสิ่งนี้ยังคงอยู่หรือไม่เมื่อ 10.0.1 ย้ายจาก trunk ไปเป็นเสถียร
Marco Bonelli

1
อีกหนึ่งความคิดเห็น: -mtune=nativeปรับแต่งสำหรับ CPU เฉพาะที่เครื่องของคุณมี นั่นจะแตกต่างกันสำหรับผู้ทดสอบที่แตกต่างกันและอาจเป็นส่วนหนึ่งของปัญหา หากคุณเรียกใช้การคอมไพล์ด้วย-vคุณควรเห็นซีพียูตระกูลใดที่อยู่บนเครื่องของคุณ (เช่น-mtune=skylakeบนคอมพิวเตอร์ของฉัน)
Nate Eldredge

1
ยังคงยากที่จะบอกจากการดีบัก ควรถอดชิ้นส่วนข้อสรุป คุณไม่จำเป็นต้องแตกไฟล์ใด ๆ เพียงค้นหาไฟล์. o ที่สร้างขึ้นเมื่อคุณคอมไพล์โปรเจ็กต์และแยกมันออก คุณสามารถใช้disassembleคำสั่งใน gdb
Nate Eldredge

5
ยังไงก็ตามขอแสดงความยินดีคุณเป็นหนึ่งในไม่กี่คนที่หายากซึ่งปัญหาที่เกิดขึ้นจริงคือข้อผิดพลาดของคอมไพเลอร์
Nate Eldredge

คำตอบ:


22

สรุป:สิ่งนี้ดูเหมือนว่าจะเป็นข้อผิดพลาดใน gcc ที่เกี่ยวข้องกับการเพิ่มประสิทธิภาพสตริง testcase ที่บรรจุในตัวเองอยู่ด้านล่าง มีข้อสงสัยบางอย่างเกี่ยวกับว่ารหัสถูกต้อง แต่ฉันคิดว่ามันเป็น

ผมได้มีการรายงานข้อผิดพลาดเป็นPR 93982 การแก้ไขที่เสนอได้รับการยอมรับแล้วแต่ไม่สามารถแก้ไขได้ในทุกกรณีนำไปสู่การติดตามผลPR 94015 ( ลิงค์ godbolt )

คุณควรจะสามารถแก้ไขข้อผิดพลาดได้ด้วยการคอมไพล์ด้วยแฟล็-fno-optimize-strlen


ฉันสามารถลดกรณีทดสอบของคุณให้เป็นตัวอย่างน้อยที่สุดต่อไปนี้ (บนgodbolt ):

struct a {
    const char ** target;
};

char* R_alloc(void);

struct a foo(void) {
    struct a res;
    res.target = (const char **) R_alloc();
    res.target[0] = "12345678";
    res.target[1] = "";
    res.target[2] = "";
    res.target[3] = "";
    res.target[4] = "";
    return res;
}

ด้วยลำต้น gcc (gcc รุ่น 10.0.1 20200225 (ทดลอง)) และ-O2(ตัวเลือกอื่น ๆ ทั้งหมดกลายเป็นสิ่งที่ไม่จำเป็น) แอสเซมบลีที่สร้างขึ้นใน amd64 จะเป็นดังนี้:

.LC0:
        .string "12345678"
.LC1:
        .string ""
foo:
        subq    $8, %rsp
        call    R_alloc
        movq    $.LC0, (%rax)
        movq    $.LC1, 16(%rax)
        movq    $.LC1, 24(%rax)
        movq    $.LC1, 32(%rax)
        addq    $8, %rsp
        ret

ดังนั้นคุณค่อนข้างถูกต้องที่คอมไพเลอร์ล้มเหลวในการเริ่มต้นres.target[1](สังเกตการขาดชัดเจนmovq $.LC1, 8(%rax))

มันน่าสนใจที่จะเล่นกับโค้ดและดูว่ามีผลต่อ "บั๊ก" อย่างไร อาจมีนัยสำคัญการเปลี่ยนประเภทการส่งคืนของR_allocเพื่อvoid *ทำให้หายไปและให้เอาต์พุตการประกอบที่ "ถูกต้อง" อาจจะน้อยกว่าอย่างเห็นได้ชัด แต่น่าขบขันมากขึ้นการเปลี่ยนสตริง"12345678"ให้ยาวขึ้นหรือสั้นลงก็ทำให้มันหายไปได้


การสนทนาก่อนหน้านี้ได้รับการแก้ไขแล้ว - รหัสถูกกฎหมาย

คำถามที่ฉันมีคือรหัสของคุณถูกกฎหมายจริงหรือไม่ ความจริงที่ว่าคุณนำสิ่งที่char *ส่งคืนมาR_alloc()และส่งไปให้const char **แล้วเก็บconst char *ดูเหมือนว่ามันอาจละเมิดกฎนามแฝงที่เข้มงวดเนื่องจากcharและconst char *ไม่ใช่ประเภทที่เข้ากันได้ มีข้อยกเว้นที่อนุญาตให้คุณเข้าถึงวัตถุใด ๆ ที่เป็นchar(เพื่อใช้สิ่งต่าง ๆ เช่นmemcpy) แต่นี่เป็นวิธีอื่น ๆ และที่ดีที่สุดที่ฉันเข้าใจคือไม่อนุญาต มันทำให้โค้ดของคุณสร้างพฤติกรรมที่ไม่ได้กำหนดดังนั้นคอมไพเลอร์สามารถทำสิ่งที่ถูกต้องตามกฎหมายได้

หากเป็นเช่นนี้การแก้ไขที่ถูกต้องจะเป็น R เพื่อเปลี่ยนรหัสของตนเพื่อให้R_alloc()ผลตอบแทนแทนvoid * char *จากนั้นจะไม่มีปัญหาเรื่องนามแฝง น่าเสียดายที่รหัสดังกล่าวอยู่นอกเหนือการควบคุมของคุณและไม่ชัดเจนสำหรับฉันว่าคุณสามารถใช้ฟังก์ชันนี้ได้อย่างไรโดยไม่ละเมิดนามแฝงที่เข้มงวด วิธีแก้ปัญหาอาจเป็นการสอดแทรกตัวแปรชั่วคราวเช่นvoid *tmp = R_alloc(); res.target = tmp;ซึ่งแก้ปัญหาในกรณีทดสอบ แต่ฉันก็ยังไม่แน่ใจว่ามันถูกกฎหมายหรือไม่

อย่างไรก็ตามฉันไม่แน่ใจเกี่ยวกับสมมุติฐาน "aliasing ที่เข้มงวด" เนื่องจากการรวบรวมด้วย-fno-strict-aliasingซึ่ง AFAIK ควรจะทำให้ gcc อนุญาตการสร้างดังกล่าวไม่ทำให้ปัญหาหายไป!


ปรับปรุง พยายามเลือกที่แตกต่างบางอย่างผมพบว่าทั้งสอง-fno-optimize-strlenหรือ-fno-tree-forwpropจะส่งผลให้ "ถูกต้อง" รหัสถูกสร้างขึ้น นอกจากนี้การใช้-O1 -foptimize-strlenให้รหัสที่ไม่ถูกต้อง (แต่-O1 -ftree-forwpropไม่ได้)

หลังจากที่เล็ก ๆ น้อย ๆgit bisectการออกกำลังกายข้อผิดพลาดที่ดูเหมือนว่าจะได้รับการแนะนำในการกระทำ 34fcf41e30ff56155e996f5e04


อัปเดต 2.ฉันพยายามขุดลงไปในแหล่ง gcc นิดหน่อยเพื่อดูว่าฉันเรียนรู้อะไรได้บ้าง (ฉันไม่ได้อ้างว่าเป็นผู้เชี่ยวชาญด้านการแปลใด ๆ !)

ดูเหมือนว่ารหัสในtree-ssa-strlen.cมีวัตถุประสงค์เพื่อติดตามสตริงที่ปรากฏในโปรแกรม ใกล้ที่สุดเท่าที่ฉันจะบอกได้ข้อผิดพลาดคือในการดูคำสั่งres.target[0] = "12345678";คอมไพเลอร์ conflates ที่อยู่ของสตริงตัวอักษร"12345678"กับสตริงตัวเอง (ดูเหมือนว่าจะเกี่ยวข้องกับรหัสที่น่าสงสัยนี้ซึ่งถูกเพิ่มเข้ามาในคอมมิชชันที่กล่าวไว้ข้างต้นซึ่งถ้ามันพยายามนับไบต์ของ "สตริง" ซึ่งเป็นที่อยู่จริงมันจะดูว่าที่อยู่นั้นชี้ไปที่ใด)

ดังนั้นจึงคิดว่าคำสั่งres.target[0] = "12345678"แทนการจัดเก็บที่อยู่ของ"12345678"ที่อยู่ที่จะจัดเก็บสตริงตัวเองตามที่อยู่ที่เป็นถ้ามีคำสั่งเป็นres.target strcpy(res.target, "12345678")หมายเหตุสำหรับสิ่งที่อยู่ข้างหน้าว่าสิ่งนี้จะส่งผลให้ nul ต่อท้ายถูกเก็บไว้ที่ที่อยู่res.target+8(ในขั้นตอนนี้ในคอมไพเลอร์ออฟเซ็ตทั้งหมดจะเป็นไบต์)

ตอนนี้เมื่อคอมไพเลอร์มีลักษณะที่res.target[1] = ""มันเหมือนกันถือว่านี้ราวกับว่ามันเป็นstrcpy(res.target+8, "")ที่ 8 char *มาจากขนาดของที่ นั่นคือราวกับว่ามันเป็นเพียงการจัดเก็บไบต์ NUL res.target+8ที่อยู่ อย่างไรก็ตามคอมไพเลอร์ "รู้" ว่าคำสั่งก่อนหน้านี้ได้เก็บ nul byte ไว้ที่แอดเดรสนั้นแล้ว! ดังนั้นคำสั่งนี้คือ "ซ้ำซ้อน" และสามารถละทิ้งได้ ( ที่นี่ )

นี่อธิบายว่าทำไมสายอักขระต้องมีความยาว 8 ตัวอักษรอย่างแน่นอนเพื่อเรียกใช้ข้อบกพร่อง (แม้ว่าตัวทวีคูณอื่น ๆ ของ 8 ยังสามารถทริกเกอร์บั๊กในสถานการณ์อื่น ๆ )


FWIW ที่นำกลับมาใช้ใหม่กับตัวชี้ประเภทอื่นจะถูกบันทึกไว้ ผมไม่ทราบว่าเกี่ยวกับ aliasing ที่จะรู้ว่าไม่ว่าจะเป็นตกลงที่จะแต่งไปแต่ไม่ได้ไปint* const char**
BrodieG

ถ้าความเข้าใจของฉัน aliasing เข้มงวดถูกต้องจากนั้นโยนไปint *ยังเป็นสิ่งผิดกฎหมาย (หรือมากกว่าจริงการจัดเก็บintที่นั่นเป็นสิ่งผิดกฎหมาย)
Nate Eldredge

1
สิ่งนี้ไม่เกี่ยวข้องกับกฎนามแฝงที่เข้มงวด กฎนามแฝงที่เข้มงวดเกี่ยวกับการเข้าถึงข้อมูลที่คุณเก็บไว้แล้วโดยใช้ตัวจัดการที่แตกต่างกัน เมื่อคุณกำหนดที่นี่เท่านั้นจะไม่ได้สัมผัสกับกฎนามแฝงที่เข้มงวด พอยน์เตอร์การหล่อนั้นใช้งานได้เมื่อทั้งสองประเภทของตัวชี้มีข้อกำหนดการจัดตำแหน่งเหมือนกัน แต่ที่นี่คุณกำลังส่งchar*และทำงานกับ x86_64 ... ฉันไม่เห็น UB ที่นี่นี่คือ gcc bug
KamilCuk

1
ใช่และไม่ใช่ @KamilCuk ในคำศัพท์มาตรฐานของ "การเข้าถึง" รวมถึงการอ่านและการปรับเปลี่ยนค่าของวัตถุ กฎนามแฝงที่เข้มงวดจึงจะพูดกับ "การจัดเก็บ" ไม่ จำกัด การดำเนินการอ่านกลับ แต่สำหรับวัตถุที่ไม่มีชนิดที่ประกาศนั่นคือความจริงที่ว่าการเขียนไปยังวัตถุดังกล่าวจะเปลี่ยนประเภทที่มีประสิทธิภาพเพื่อให้สอดคล้องกับสิ่งที่ถูกเขียนโดยอัตโนมัติ วัตถุที่ไม่มีประเภทที่ประกาศนั้นเป็นวัตถุที่ถูกจัดสรรแบบไดนามิก (โดยไม่คำนึงถึงประเภทของตัวชี้ที่พวกเขาเข้าถึง) ดังนั้นจึงไม่มีการละเมิด SA ที่นี่
John Bollinger

2
ใช่ @Nate ด้วยคำจำกัดความของR_alloc()โปรแกรมนั้นเป็นไปตามข้อกำหนดโดยไม่คำนึงถึงหน่วยการแปลที่R_alloc()กำหนดไว้ มันเป็นคอมไพเลอร์ที่ล้มเหลวที่จะปฏิบัติตามที่นี่
John Bollinger
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.